Skip to content

Commit 87727b7

Browse files
committed
Add expires_at, registered_at, updated_at, and expires_in_days properties to Dates model
1 parent 25b7e95 commit 87727b7

File tree

2 files changed

+64
-0
lines changed

2 files changed

+64
-0
lines changed

src/rdapapi/models.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from datetime import datetime, timezone
56
from typing import List, Optional
67

78
from pydantic import BaseModel, Field
@@ -47,6 +48,38 @@ class Dates(BaseModel):
4748
expires: Optional[str] = None
4849
updated: Optional[str] = None
4950

51+
@staticmethod
52+
def _parse(value: Optional[str]) -> Optional[datetime]:
53+
if value is None:
54+
return None
55+
try:
56+
return datetime.fromisoformat(value)
57+
except (ValueError, TypeError):
58+
return None
59+
60+
@property
61+
def registered_at(self) -> Optional[datetime]:
62+
"""Parse ``registered`` into a timezone-aware :class:`~datetime.datetime`."""
63+
return self._parse(self.registered)
64+
65+
@property
66+
def expires_at(self) -> Optional[datetime]:
67+
"""Parse ``expires`` into a timezone-aware :class:`~datetime.datetime`."""
68+
return self._parse(self.expires)
69+
70+
@property
71+
def updated_at(self) -> Optional[datetime]:
72+
"""Parse ``updated`` into a timezone-aware :class:`~datetime.datetime`."""
73+
return self._parse(self.updated)
74+
75+
@property
76+
def expires_in_days(self) -> Optional[int]:
77+
"""Days until expiration, or ``None`` if no expiry date is available."""
78+
dt = self.expires_at
79+
if dt is None:
80+
return None
81+
return (dt - datetime.now(timezone.utc)).days
82+
5083

5184
class Registrar(BaseModel):
5285
"""Domain registrar information."""

tests/test_models.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from rdapapi import (
44
AsnResponse,
55
BulkDomainResponse,
6+
Dates,
67
DomainResponse,
78
EntityResponse,
89
IpResponse,
@@ -257,6 +258,36 @@ def test_model_dump_roundtrip():
257258
assert restored.handle == "AS15169"
258259

259260

261+
def test_dates_parsed_properties():
262+
from datetime import datetime, timezone
263+
264+
dates = Dates.model_validate(
265+
{"registered": "2020-01-01T00:00:00Z", "expires": "2028-09-14T04:00:00Z", "updated": None}
266+
)
267+
268+
assert dates.registered_at == datetime(2020, 1, 1, tzinfo=timezone.utc)
269+
assert dates.expires_at == datetime(2028, 9, 14, 4, 0, 0, tzinfo=timezone.utc)
270+
assert dates.updated_at is None
271+
assert isinstance(dates.expires_in_days, int)
272+
assert dates.expires_in_days > 0
273+
274+
275+
def test_dates_null_returns_none():
276+
dates = Dates.model_validate({"registered": None, "expires": None, "updated": None})
277+
278+
assert dates.registered_at is None
279+
assert dates.expires_at is None
280+
assert dates.updated_at is None
281+
assert dates.expires_in_days is None
282+
283+
284+
def test_dates_invalid_string_returns_none():
285+
dates = Dates.model_validate({"registered": "not-a-date", "expires": "garbage", "updated": None})
286+
287+
assert dates.registered_at is None
288+
assert dates.expires_at is None
289+
290+
260291
def test_bulk_domain_response_parses():
261292
data = {
262293
"results": [

0 commit comments

Comments
 (0)