|
1 | | -"""Data models for deprecation entries.""" |
| 1 | +"""Deprecation model for AI model deprecation tracking.""" |
2 | 2 |
|
3 | | -import hashlib |
4 | 3 | from datetime import UTC, datetime |
5 | | -from typing import Any |
6 | 4 |
|
7 | | -from pydantic import BaseModel, Field, HttpUrl, field_validator, model_validator |
| 5 | +from pydantic import BaseModel, Field, field_validator, model_validator |
8 | 6 |
|
9 | 7 |
|
10 | | -class Deprecation(BaseModel): |
11 | | - """Model for AI model deprecation information.""" |
| 8 | +class DeprecationEntry(BaseModel): |
| 9 | + """Model representing an AI model deprecation entry.""" |
12 | 10 |
|
13 | | - provider: str = Field(description="Provider name (e.g., 'OpenAI', 'Anthropic')") |
14 | | - model: str = Field(description="Affected model name") |
15 | | - deprecation_date: datetime = Field(description="When the deprecation was announced") |
16 | | - retirement_date: datetime = Field(description="When the model stops working") |
17 | | - replacement: str | None = Field(default=None, description="Suggested alternative model") |
18 | | - notes: str | None = Field(default=None, description="Additional context") |
19 | | - source_url: HttpUrl = Field(description="URL where the deprecation info came from") |
| 11 | + provider: str = Field(..., description="Provider name (e.g., OpenAI, Anthropic)") |
| 12 | + model: str = Field(..., description="Model name or identifier") |
| 13 | + deprecation_date: datetime = Field(..., description="Date when deprecation was announced") |
| 14 | + retirement_date: datetime = Field(..., description="Date when model stops working") |
| 15 | + replacement: str | None = Field(None, description="Suggested alternative model") |
| 16 | + notes: str | None = Field(None, description="Additional context or information") |
| 17 | + source_url: str = Field(..., description="Link to official announcement") |
20 | 18 | last_updated: datetime = Field( |
21 | 19 | default_factory=lambda: datetime.now(UTC), |
22 | | - description="When we last checked this information", |
23 | | - ) |
24 | | - # Alias for compatibility with main branch |
25 | | - created_at: datetime = Field( |
26 | | - default_factory=lambda: datetime.now(UTC), |
27 | | - description="When entry was created (alias for last_updated)", |
| 20 | + description="When this entry was last updated", |
28 | 21 | ) |
29 | 22 |
|
30 | | - @field_validator("deprecation_date", "retirement_date", "last_updated") |
| 23 | + @field_validator("provider", "model") |
| 24 | + @classmethod |
| 25 | + def validate_non_empty_string(cls, v: str) -> str: |
| 26 | + """Ensure provider and model are non-empty strings.""" |
| 27 | + if not v or not v.strip(): |
| 28 | + raise ValueError("Field must be a non-empty string") |
| 29 | + return v.strip() |
| 30 | + |
| 31 | + @field_validator("source_url") |
31 | 32 | @classmethod |
32 | | - def ensure_utc_timezone(cls, v: datetime) -> datetime: |
33 | | - """Ensure datetime fields have UTC timezone.""" |
34 | | - if v.tzinfo is None: |
35 | | - return v.replace(tzinfo=UTC) |
36 | | - return v.astimezone(UTC) |
| 33 | + def validate_url(cls, v: str) -> str: |
| 34 | + """Basic URL validation.""" |
| 35 | + if not v.startswith(("http://", "https://")): |
| 36 | + raise ValueError("URL must start with http:// or https://") |
| 37 | + return v |
37 | 38 |
|
38 | 39 | @model_validator(mode="after") |
39 | | - def validate_dates(self) -> "Deprecation": |
40 | | - """Validate that retirement_date is after deprecation_date.""" |
| 40 | + def validate_dates(self) -> "DeprecationEntry": |
| 41 | + """Ensure retirement date is after deprecation date.""" |
41 | 42 | if self.retirement_date <= self.deprecation_date: |
42 | | - raise ValueError("retirement_date must be after deprecation_date") |
| 43 | + raise ValueError("Retirement date must be after deprecation date") |
43 | 44 | return self |
44 | 45 |
|
| 46 | + def to_rss_item(self) -> dict[str, str | datetime]: |
| 47 | + """Convert to RSS item dictionary.""" |
| 48 | + description_parts = [ |
| 49 | + f"Provider: {self.provider}", |
| 50 | + f"Model: {self.model}", |
| 51 | + f"Deprecation Date: {self.deprecation_date.isoformat()}", |
| 52 | + f"Retirement Date: {self.retirement_date.isoformat()}", |
| 53 | + ] |
| 54 | + |
| 55 | + if self.replacement: |
| 56 | + description_parts.append(f"Replacement: {self.replacement}") |
| 57 | + |
| 58 | + if self.notes: |
| 59 | + description_parts.append(f"Notes: {self.notes}") |
| 60 | + |
| 61 | + description = "\n".join(description_parts) |
| 62 | + |
| 63 | + title = f"{self.provider} - {self.model} Deprecation" |
| 64 | + |
| 65 | + return { |
| 66 | + "title": title, |
| 67 | + "description": description, |
| 68 | + "link": self.source_url, |
| 69 | + "guid": f"{self.provider}-{self.model}-{self.deprecation_date.isoformat()}", |
| 70 | + "pubDate": self.deprecation_date, |
| 71 | + } |
| 72 | + |
| 73 | + def to_json_dict(self) -> dict[str, str | None]: |
| 74 | + """Convert to JSON-serializable dictionary.""" |
| 75 | + return { |
| 76 | + "provider": self.provider, |
| 77 | + "model": self.model, |
| 78 | + "deprecation_date": self.deprecation_date.isoformat(), |
| 79 | + "retirement_date": self.retirement_date.isoformat(), |
| 80 | + "replacement": self.replacement, |
| 81 | + "notes": self.notes, |
| 82 | + "source_url": self.source_url, |
| 83 | + "last_updated": self.last_updated.isoformat(), |
| 84 | + } |
| 85 | + |
| 86 | + model_config = { |
| 87 | + "json_encoders": { |
| 88 | + datetime: lambda v: v.isoformat(), |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + # Compatibility methods for main branch |
| 93 | + def is_active(self) -> bool: |
| 94 | + """Check if deprecation is still active (not yet retired).""" |
| 95 | + now = datetime.now(UTC) |
| 96 | + return self.retirement_date > now |
| 97 | + |
45 | 98 | def get_hash(self) -> str: |
46 | | - """Generate hash of core deprecation data (excluding last_updated).""" |
47 | | - # Include all fields that identify the unique deprecation, excluding last_updated |
| 99 | + """Generate hash for core deprecation data.""" |
| 100 | + import hashlib |
| 101 | + |
48 | 102 | core_data = { |
49 | 103 | "provider": self.provider, |
50 | 104 | "model": self.model, |
51 | 105 | "deprecation_date": self.deprecation_date.isoformat(), |
52 | 106 | "retirement_date": self.retirement_date.isoformat(), |
53 | 107 | "replacement": self.replacement, |
54 | 108 | "notes": self.notes, |
55 | | - "source_url": str(self.source_url), |
| 109 | + "source_url": self.source_url, |
56 | 110 | } |
57 | | - |
58 | | - # Create deterministic string representation |
59 | 111 | data_str = str(sorted(core_data.items())) |
60 | 112 | return hashlib.sha256(data_str.encode()).hexdigest() |
61 | 113 |
|
62 | 114 | def get_identity_hash(self) -> str: |
63 | | - """Generate hash for identifying same deprecation (for updates).""" |
64 | | - # Only include immutable fields that identify the unique deprecation |
| 115 | + """Generate hash for identifying same deprecation.""" |
| 116 | + import hashlib |
| 117 | + |
65 | 118 | identity_data = { |
66 | 119 | "provider": self.provider, |
67 | 120 | "model": self.model, |
68 | 121 | "deprecation_date": self.deprecation_date.isoformat(), |
69 | 122 | "retirement_date": self.retirement_date.isoformat(), |
70 | | - "source_url": str(self.source_url), |
| 123 | + "source_url": self.source_url, |
71 | 124 | } |
72 | | - |
73 | | - # Create deterministic string representation |
74 | 125 | data_str = str(sorted(identity_data.items())) |
75 | 126 | return hashlib.sha256(data_str.encode()).hexdigest() |
76 | 127 |
|
77 | | - def same_deprecation(self, other: "Deprecation") -> bool: |
78 | | - """Check if this represents the same deprecation (for updates).""" |
| 128 | + def same_deprecation(self, other: "DeprecationEntry") -> bool: |
| 129 | + """Check if this represents the same deprecation.""" |
79 | 130 | return self.get_identity_hash() == other.get_identity_hash() |
80 | 131 |
|
81 | 132 | def __eq__(self, other: object) -> bool: |
82 | 133 | """Compare deprecations based on core data (excluding last_updated).""" |
83 | | - if not isinstance(other, Deprecation): |
| 134 | + if not isinstance(other, DeprecationEntry): |
84 | 135 | return False |
85 | 136 | return self.get_hash() == other.get_hash() |
86 | 137 |
|
87 | | - def __hash__(self) -> int: |
88 | | - """Hash based on core data for use in sets/dicts.""" |
89 | | - return hash(self.get_hash()) |
90 | | - |
91 | 138 | def __str__(self) -> str: |
92 | 139 | """String representation of deprecation.""" |
93 | 140 | return ( |
94 | | - f"Deprecation({self.provider} {self.model}: " |
95 | | - f"{self.deprecation_date.date()} -> {self.retirement_date.date()})" |
| 141 | + f"DeprecationEntry(provider='{self.provider}', model='{self.model}', " |
| 142 | + f"deprecation_date='{self.deprecation_date.date()}', " |
| 143 | + f"retirement_date='{self.retirement_date.date()}')" |
96 | 144 | ) |
97 | 145 |
|
98 | | - def is_active(self) -> bool: |
99 | | - """Check if the deprecation is still active (not yet retired).""" |
100 | | - now = datetime.now(UTC) |
101 | | - return self.retirement_date > now |
102 | | - |
103 | | - def to_rss_item(self) -> dict[str, Any]: |
104 | | - """Convert deprecation to RSS item format (compatibility with main).""" |
105 | | - title = f"{self.provider}: {self.model} Deprecation" |
106 | | - description_parts = [ |
107 | | - f"Model: {self.model}", |
108 | | - f"Provider: {self.provider}", |
109 | | - f"Deprecation Date: {self.deprecation_date.strftime('%Y-%m-%d')}", |
110 | | - f"Retirement Date: {self.retirement_date.strftime('%Y-%m-%d')}", |
111 | | - ] |
112 | | - if self.replacement: |
113 | | - description_parts.append(f"Replacement: {self.replacement}") |
114 | | - if self.notes: |
115 | | - description_parts.append(f"Notes: {self.notes}") |
116 | | - |
117 | | - return { |
118 | | - "title": title, |
119 | | - "description": " | ".join(description_parts), |
120 | | - "guid": str(self.source_url), |
121 | | - "pubDate": self.created_at, |
122 | | - "link": str(self.source_url), |
123 | | - } |
124 | | - |
125 | | - def __repr__(self) -> str: |
126 | | - """Detailed string representation.""" |
127 | | - return ( |
128 | | - f"Deprecation(provider='{self.provider}', model='{self.model}', " |
129 | | - f"deprecation_date={self.deprecation_date.isoformat()}, " |
130 | | - f"retirement_date={self.retirement_date.isoformat()})" |
131 | | - ) |
| 146 | + @property |
| 147 | + def created_at(self) -> datetime: |
| 148 | + """Alias for compatibility with main branch.""" |
| 149 | + return self.deprecation_date |
132 | 150 |
|
133 | 151 |
|
134 | | -# Main has DeprecationEntry, we use Deprecation, create alias for compatibility |
135 | | -DeprecationEntry = Deprecation |
| 152 | +# Alias for compatibility with other branches that may use Deprecation |
| 153 | +Deprecation = DeprecationEntry |
0 commit comments