diff --git a/README.md b/README.md index 9760759..0576f53 100644 --- a/README.md +++ b/README.md @@ -389,6 +389,26 @@ params = ( regions = client.regions().list(params).data ``` +## Date and Datetime Fields + +The SDK distinguishes between date-only and datetime fields: + +- **Datetime fields** are deserialized as `datetime.datetime` with `timezone.utc`: + - All `created_at` fields — present on most resources + - Expiry fields: `Did.expires_at`, `DidReservation.expire_at`, `Proof.expires_at`, `EncryptedFile.expire_at` +- **Date-only fields** (`Identity.birth_date`, `CapacityPool.renew_date`, `DidOrderItem.billed_from`/`billed_to`) remain as `string` in `"YYYY-MM-DD"` format. + +```python +from datetime import timezone + +did = client.dids().find("uuid").data +print(did.created_at) # datetime(2024, 1, 15, 10, 0, 0, tzinfo=timezone.utc) +print(did.expires_at) # None or datetime(...) + +identity = client.identities().find("uuid").data +print(identity.birth_date) # "1990-05-20" +``` + ## Enums The SDK provides enum classes aligned with the Java SDK (for example `CallbackMethod`, `IdentityType`, `OrderStatus`, `ExportType`, `CliFormat`, `OnCliMismatchAction`, `MediaEncryptionMode`, `TransportProtocol`, `Codec`, and more). diff --git a/src/didww/resources/address.py b/src/didww/resources/address.py index e13ce1d..23589df 100644 --- a/src/didww/resources/address.py +++ b/src/didww/resources/address.py @@ -1,4 +1,4 @@ -from didww.resources.base import DidwwApiModel, SafeAttributeField, RelationField, Repository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, RelationField, Repository class Address(DidwwApiModel): @@ -8,7 +8,7 @@ class Address(DidwwApiModel): postal_code = SafeAttributeField("postal_code") address = SafeAttributeField("address") description = SafeAttributeField("description") - created_at = SafeAttributeField("created_at") + created_at = DatetimeAttributeField("created_at") verified = SafeAttributeField("verified") country = RelationField("country") diff --git a/src/didww/resources/address_verification.py b/src/didww/resources/address_verification.py index 96fea9f..18fa15f 100644 --- a/src/didww/resources/address_verification.py +++ b/src/didww/resources/address_verification.py @@ -1,5 +1,5 @@ from didww.enums import AddressVerificationStatus, CallbackMethod -from didww.resources.base import DidwwApiModel, SafeAttributeField, EnumAttributeField, RelationField, CreateOnlyRepository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, EnumAttributeField, RelationField, CreateOnlyRepository class _SplitSemicolonField(SafeAttributeField): @@ -21,7 +21,7 @@ class AddressVerification(DidwwApiModel): service_description = SafeAttributeField("service_description") reject_reasons = _SplitSemicolonField("reject_reasons") reference = SafeAttributeField("reference") - created_at = SafeAttributeField("created_at") + created_at = DatetimeAttributeField("created_at") address = RelationField("address") dids = RelationField("dids") diff --git a/src/didww/resources/base.py b/src/didww/resources/base.py index 7546d69..8e91e90 100644 --- a/src/didww/resources/base.py +++ b/src/didww/resources/base.py @@ -1,5 +1,6 @@ import functools import importlib +from datetime import datetime, timezone from enum import Enum from jsonapi_requests.orm.api import OrmApi @@ -80,6 +81,24 @@ def __set__(self, instance, value): instance.attributes[self.source] = value +class DatetimeAttributeField(SafeAttributeField): + """AttributeField that parses ISO 8601 datetime strings to datetime objects. + + No custom setter is defined because all current datetime fields are read-only + (e.g. created_at). The inherited setter stores values as-is. If a writable + datetime field is added in the future, a setter that converts datetime back + to an ISO 8601 string should be implemented here. + """ + + def __get__(self, instance, type=None): + raw = super().__get__(instance, type) + if instance is None: + return self + if raw is None: + return None + return datetime.fromisoformat(raw.replace("Z", "+00:00")) # "Z" not supported by fromisoformat before Python 3.11 + + class EnumAttributeField(SafeAttributeField): """AttributeField that serializes/deserializes Enum values.""" diff --git a/src/didww/resources/did.py b/src/didww/resources/did.py index b9d18cf..93b5d60 100644 --- a/src/didww/resources/did.py +++ b/src/didww/resources/did.py @@ -1,4 +1,4 @@ -from didww.resources.base import DidwwApiModel, SafeAttributeField, RelationField, ExclusiveRelationField, Repository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, RelationField, ExclusiveRelationField, Repository class Did(DidwwApiModel): _writable_attrs = { @@ -12,8 +12,8 @@ class Did(DidwwApiModel): description = SafeAttributeField("description") terminated = SafeAttributeField("terminated") awaiting_registration = SafeAttributeField("awaiting_registration") - created_at = SafeAttributeField("created_at") - expires_at = SafeAttributeField("expires_at") + created_at = DatetimeAttributeField("created_at") + expires_at = DatetimeAttributeField("expires_at") channels_included_count = SafeAttributeField("channels_included_count") billing_cycles_count = SafeAttributeField("billing_cycles_count") dedicated_channels_count = SafeAttributeField("dedicated_channels_count") diff --git a/src/didww/resources/did_reservation.py b/src/didww/resources/did_reservation.py index c3ff12e..3a127d5 100644 --- a/src/didww/resources/did_reservation.py +++ b/src/didww/resources/did_reservation.py @@ -1,12 +1,12 @@ from didww.exceptions import DidwwApiError -from didww.resources.base import DidwwApiModel, SafeAttributeField, RelationField, Repository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, RelationField, Repository class DidReservation(DidwwApiModel): _writable_attrs = {"description"} - expire_at = SafeAttributeField("expire_at") - created_at = SafeAttributeField("created_at") + expire_at = DatetimeAttributeField("expire_at") + created_at = DatetimeAttributeField("created_at") description = SafeAttributeField("description") available_did = RelationField("available_did") diff --git a/src/didww/resources/encrypted_file.py b/src/didww/resources/encrypted_file.py index 0ae79a0..f274eee 100644 --- a/src/didww/resources/encrypted_file.py +++ b/src/didww/resources/encrypted_file.py @@ -1,9 +1,9 @@ -from didww.resources.base import DidwwApiModel, SafeAttributeField, Repository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, Repository class EncryptedFile(DidwwApiModel): description = SafeAttributeField("description") - expire_at = SafeAttributeField("expire_at") + expire_at = DatetimeAttributeField("expire_at") class Meta: type = "encrypted_files" diff --git a/src/didww/resources/export.py b/src/didww/resources/export.py index 4e35732..2bb380f 100644 --- a/src/didww/resources/export.py +++ b/src/didww/resources/export.py @@ -1,12 +1,12 @@ from didww.enums import ExportStatus, ExportType, CallbackMethod -from didww.resources.base import DidwwApiModel, SafeAttributeField, EnumAttributeField, CreateOnlyRepository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, EnumAttributeField, CreateOnlyRepository class Export(DidwwApiModel): _writable_attrs = {"filters", "export_type", "callback_url", "callback_method"} status = EnumAttributeField("status", ExportStatus) - created_at = SafeAttributeField("created_at") + created_at = DatetimeAttributeField("created_at") url = SafeAttributeField("url") callback_url = SafeAttributeField("callback_url") callback_method = EnumAttributeField("callback_method", CallbackMethod) diff --git a/src/didww/resources/identity.py b/src/didww/resources/identity.py index 50b53ac..5dcf031 100644 --- a/src/didww/resources/identity.py +++ b/src/didww/resources/identity.py @@ -1,5 +1,5 @@ from didww.enums import IdentityType -from didww.resources.base import DidwwApiModel, SafeAttributeField, EnumAttributeField, RelationField, Repository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, EnumAttributeField, RelationField, Repository class Identity(DidwwApiModel): @@ -22,7 +22,7 @@ class Identity(DidwwApiModel): identity_type = EnumAttributeField("identity_type", IdentityType) contact_email = SafeAttributeField("contact_email") external_reference_id = SafeAttributeField("external_reference_id") - created_at = SafeAttributeField("created_at") + created_at = DatetimeAttributeField("created_at") verified = SafeAttributeField("verified") country = RelationField("country") diff --git a/src/didww/resources/order.py b/src/didww/resources/order.py index dbbec9f..6156f4e 100644 --- a/src/didww/resources/order.py +++ b/src/didww/resources/order.py @@ -1,5 +1,5 @@ from didww.enums import CallbackMethod, OrderStatus -from didww.resources.base import DidwwApiModel, SafeAttributeField, EnumAttributeField, Repository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, EnumAttributeField, Repository from didww.resources.order_item.base import OrderItem # Import to register types @@ -15,7 +15,7 @@ class Order(DidwwApiModel): amount = SafeAttributeField("amount") status = EnumAttributeField("status", OrderStatus) - created_at = SafeAttributeField("created_at") + created_at = DatetimeAttributeField("created_at") description = SafeAttributeField("description") reference = SafeAttributeField("reference") callback_url = SafeAttributeField("callback_url") diff --git a/src/didww/resources/permanent_supporting_document.py b/src/didww/resources/permanent_supporting_document.py index c0b380e..4796b23 100644 --- a/src/didww/resources/permanent_supporting_document.py +++ b/src/didww/resources/permanent_supporting_document.py @@ -1,10 +1,10 @@ -from didww.resources.base import DidwwApiModel, SafeAttributeField, RelationField, CreateOnlyRepository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, RelationField, CreateOnlyRepository class PermanentSupportingDocument(DidwwApiModel): _writable_attrs = set() - created_at = SafeAttributeField("created_at") + created_at = DatetimeAttributeField("created_at") identity = RelationField("identity") template = RelationField("template") diff --git a/src/didww/resources/proof.py b/src/didww/resources/proof.py index 9e46643..247c189 100644 --- a/src/didww/resources/proof.py +++ b/src/didww/resources/proof.py @@ -1,11 +1,11 @@ -from didww.resources.base import DidwwApiModel, SafeAttributeField, RelationField, CreateOnlyRepository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, RelationField, CreateOnlyRepository class Proof(DidwwApiModel): _writable_attrs = set() - created_at = SafeAttributeField("created_at") - expires_at = SafeAttributeField("expires_at") + created_at = DatetimeAttributeField("created_at") + expires_at = DatetimeAttributeField("expires_at") proof_type = RelationField("proof_type") entity = RelationField("entity") diff --git a/src/didww/resources/shared_capacity_group.py b/src/didww/resources/shared_capacity_group.py index 3fefc46..0f2e823 100644 --- a/src/didww/resources/shared_capacity_group.py +++ b/src/didww/resources/shared_capacity_group.py @@ -1,4 +1,4 @@ -from didww.resources.base import DidwwApiModel, SafeAttributeField, RelationField, Repository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, RelationField, Repository class SharedCapacityGroup(DidwwApiModel): @@ -7,7 +7,7 @@ class SharedCapacityGroup(DidwwApiModel): name = SafeAttributeField("name") shared_channels_count = SafeAttributeField("shared_channels_count") metered_channels_count = SafeAttributeField("metered_channels_count") - created_at = SafeAttributeField("created_at") + created_at = DatetimeAttributeField("created_at") capacity_pool = RelationField("capacity_pool") dids = RelationField("dids") diff --git a/src/didww/resources/voice_in_trunk.py b/src/didww/resources/voice_in_trunk.py index debe190..9c2a6b6 100644 --- a/src/didww/resources/voice_in_trunk.py +++ b/src/didww/resources/voice_in_trunk.py @@ -1,5 +1,5 @@ from didww.enums import CliFormat -from didww.resources.base import DidwwApiModel, SafeAttributeField, EnumAttributeField, RelationField, Repository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, EnumAttributeField, RelationField, Repository from didww.resources.configuration.base import TrunkConfiguration # Import to register types @@ -21,7 +21,7 @@ class VoiceInTrunk(DidwwApiModel): cli_prefix = SafeAttributeField("cli_prefix") description = SafeAttributeField("description") ringing_timeout = SafeAttributeField("ringing_timeout") - created_at = SafeAttributeField("created_at") + created_at = DatetimeAttributeField("created_at") pop = RelationField("pop") voice_in_trunk_group = RelationField("voice_in_trunk_group") diff --git a/src/didww/resources/voice_in_trunk_group.py b/src/didww/resources/voice_in_trunk_group.py index 3ed21ed..3faed54 100644 --- a/src/didww/resources/voice_in_trunk_group.py +++ b/src/didww/resources/voice_in_trunk_group.py @@ -1,4 +1,4 @@ -from didww.resources.base import DidwwApiModel, SafeAttributeField, RelationField, Repository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, RelationField, Repository class VoiceInTrunkGroup(DidwwApiModel): @@ -6,7 +6,7 @@ class VoiceInTrunkGroup(DidwwApiModel): name = SafeAttributeField("name") capacity_limit = SafeAttributeField("capacity_limit") - created_at = SafeAttributeField("created_at") + created_at = DatetimeAttributeField("created_at") voice_in_trunks = RelationField("voice_in_trunks") diff --git a/src/didww/resources/voice_out_trunk.py b/src/didww/resources/voice_out_trunk.py index d1647d4..c58d6de 100644 --- a/src/didww/resources/voice_out_trunk.py +++ b/src/didww/resources/voice_out_trunk.py @@ -4,7 +4,7 @@ OnCliMismatchAction, VoiceOutTrunkStatus, ) -from didww.resources.base import DidwwApiModel, SafeAttributeField, EnumAttributeField, RelationField, Repository +from didww.resources.base import DidwwApiModel, DatetimeAttributeField, SafeAttributeField, EnumAttributeField, RelationField, Repository class VoiceOutTrunk(DidwwApiModel): @@ -32,7 +32,7 @@ class VoiceOutTrunk(DidwwApiModel): callback_url = SafeAttributeField("callback_url") username = SafeAttributeField("username") password = SafeAttributeField("password") - created_at = SafeAttributeField("created_at") + created_at = DatetimeAttributeField("created_at") default_did = RelationField("default_did") dids = RelationField("dids") diff --git a/tests/resources/test_address_verification.py b/tests/resources/test_address_verification.py index 1c69757..466ac4d 100644 --- a/tests/resources/test_address_verification.py +++ b/tests/resources/test_address_verification.py @@ -1,3 +1,5 @@ +from datetime import datetime, timezone + from tests.conftest import my_vcr from didww.enums import AddressVerificationStatus, CallbackMethod, IdentityType from didww.query_params import QueryParams @@ -25,7 +27,7 @@ def test_find_address_verification(self, client): assert av.id == "c8e004b0-87ec-4987-b4fb-ee89db099f0e" assert av.status == AddressVerificationStatus.APPROVED assert av.reference == "SHB-485120" - assert av.created_at == "2020-09-15T06:38:12.650Z" + assert av.created_at == datetime(2020, 9, 15, 6, 38, 12, 650000, tzinfo=timezone.utc) @my_vcr.use_cassette("address_verifications/show_rejected.yaml") def test_find_rejected_address_verification(self, client): @@ -35,7 +37,7 @@ def test_find_rejected_address_verification(self, client): assert av.status == AddressVerificationStatus.REJECTED assert av.reject_reasons == ["Address cannot be validated", "Proof of address should be not older than of 6 months"] assert av.reference == "ODW-879912" - assert av.created_at == "2020-10-28T08:29:29.960Z" + assert av.created_at == datetime(2020, 10, 28, 8, 29, 29, 960000, tzinfo=timezone.utc) @my_vcr.use_cassette("address_verifications/show_with_includes.yaml") def test_find_address_verification_with_includes(self, client):