Repository: hikari-py/hikari
Pull request #2149 (2025-01-23 01:36:13)

Fix entitlement starts_at and ends_at erroring when null
### Summary
Entitlement `starts_at` and `ends_at` are nullable in the Discord API, and if it's null, results in this error:
```
ends_at=time.iso8601_datetime_string_to_datetime(payload["ends_at"]) if "ends_at" in payload else None,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/python3.12/site-packages/hikari/internal/time.py", line 81, in slow_iso8601_datetime_string_to_datetime
    if datetime_str.endswith(("z", "Z")):
        ^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'endswith'
```

The error for `starts_at` is similar.

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.

### Related issues
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->



Patch:
diff --git a/changes/2149.bugfix.md b/changes/2149.bugfix.md
new file mode 100644
index 00000000..987511b7
--- /dev/null
+++ b/changes/2149.bugfix.md
@@ -0,0 +1,1 @@
+Fixed Entitlement `starts_at` and `ends_at` erroring when set to `None`.
diff --git a/dev-requirements.txt b/dev-requirements.txt
index cbfbbfc0..4cb19302 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,1 +1,1 @@
-nox[uv]==2024.10.9
+nox[uv]==2025.2.9
diff --git a/dev-requirements/coverage.txt b/dev-requirements/coverage.txt
index e668f7d1..5f600886 100644
--- a/dev-requirements/coverage.txt
+++ b/dev-requirements/coverage.txt
@@ -1,1 +1,1 @@
-coverage[toml]==7.6.10
+coverage[toml]==7.6.12
diff --git a/dev-requirements/pyright.txt b/dev-requirements/pyright.txt
index 66f07fd2..7b1b0698 100644
--- a/dev-requirements/pyright.txt
+++ b/dev-requirements/pyright.txt
@@ -1,1 +1,1 @@
-pyright==1.1.392.post0
+pyright==1.1.394
diff --git a/dev-requirements/release.txt b/dev-requirements/release.txt
index 1d86da8e..b441f943 100644
--- a/dev-requirements/release.txt
+++ b/dev-requirements/release.txt
@@ -1,3 +1,3 @@
-twine==6.0.1
+twine==6.1.0
 
 -r build.txt
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index bb364022..a81fe8d4 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -3755,7 +3755,7 @@ def deserialize_webhook(self, payload: data_binding.JSONObject) -> webhook_model
     ##################
 
     def deserialize_entitlement(self, payload: data_binding.JSONObject) -> monetization_models.Entitlement:
-        starts_at = time.iso8601_datetime_string_to_datetime(payload["starts_at"]) if "starts_at" in payload else None
+        starts_at = time.iso8601_datetime_string_to_datetime(payload["starts_at"]) if payload.get("starts_at") else None
 
         return monetization_models.Entitlement(
             id=snowflakes.Snowflake(payload["id"]),
@@ -3766,7 +3766,7 @@ def deserialize_entitlement(self, payload: data_binding.JSONObject) -> monetizat
             user_id=snowflakes.Snowflake(payload["user_id"]) if "user_id" in payload else None,
             is_deleted=payload["deleted"],
             starts_at=starts_at,
-            ends_at=time.iso8601_datetime_string_to_datetime(payload["ends_at"]) if "ends_at" in payload else None,
+            ends_at=time.iso8601_datetime_string_to_datetime(payload["ends_at"]) if payload.get("ends_at") else None,
             subscription_id=snowflakes.Snowflake(payload["subscription_id"]) if "subscription_id" in payload else None,
         )
 


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index d89ba045..67d116be 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -7169,6 +7169,21 @@ def entitlement_payload(self):
             "subscription_id": "1019653835926409216",
         }
 
+    @pytest.fixture
+    def entitlement_payload_starts_ends_null(self):
+        return {
+            "id": "696969696969696",
+            "sku_id": "420420420420420",
+            "application_id": "123123123123123",
+            "type": 8,
+            "deleted": False,
+            "starts_at": None,
+            "ends_at": None,
+            "guild_id": "1015034326372454400",
+            "user_id": "115590097100865541",
+            "subscription_id": "1019653835926409216",
+        }
+
     @pytest.fixture
     def sku_payload(self):
         return {
@@ -7195,6 +7210,21 @@ def test_deserialize_entitlement(self, entity_factory_impl, entitlement_payload)
         assert entitlement.subscription_id == 1019653835926409216
         assert isinstance(entitlement, monetization_models.Entitlement)
 
+    def test_deserialize_entitlement_starts_ends_null(self, entity_factory_impl, entitlement_payload_starts_ends_null):
+        entitlement = entity_factory_impl.deserialize_entitlement(entitlement_payload_starts_ends_null)
+
+        assert entitlement.id == 696969696969696
+        assert entitlement.sku_id == 420420420420420
+        assert entitlement.application_id == 123123123123123
+        assert entitlement.type is monetization_models.EntitlementType.APPLICATION_SUBSCRIPTION
+        assert entitlement.is_deleted is False
+        assert entitlement.starts_at is None
+        assert entitlement.ends_at is None
+        assert entitlement.guild_id == 1015034326372454400
+        assert entitlement.user_id == 115590097100865541
+        assert entitlement.subscription_id == 1019653835926409216
+        assert isinstance(entitlement, monetization_models.Entitlement)
+
     def test_deserialize_sku(self, entity_factory_impl, sku_payload):
         sku = entity_factory_impl.deserialize_sku(sku_payload)