Skip to content

Commit 3df8b10

Browse files
hive-botclaude
andcommitted
fix: return None for absent developer key labels and usage limits
DeveloperKey.label and KeyUsage.api_key_label were returned as an empty string when the API sent none, and an absent usage_limits read as a DeveloperKeyUsageLimits with every limit unlimited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c32eebf commit 3df8b10

2 files changed

Lines changed: 40 additions & 18 deletions

File tree

‎deepl/api_data.py‎

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2511,21 +2511,22 @@ class DeveloperKey:
25112511
"""Information about a developer API key, as returned by the Admin API.
25122512
25132513
:param key_id: Unique ID of the key.
2514-
:param label: Label assigned to the key.
2514+
:param label: Label assigned to the key, or None if absent.
25152515
:param creation_time: Timestamp when the key was created.
25162516
:param deactivated_time: Timestamp when the key was deactivated, if it was.
25172517
:param is_deactivated: True if the key is deactivated.
2518-
:param usage_limits: Usage limits configured for the key.
2518+
:param usage_limits: Usage limits configured for the key, or None if
2519+
absent.
25192520
"""
25202521

25212522
def __init__(
25222523
self,
25232524
key_id: str,
2524-
label: str,
2525+
label: Optional[str],
25252526
creation_time: Optional[datetime.datetime],
25262527
deactivated_time: Optional[datetime.datetime],
25272528
is_deactivated: bool,
2528-
usage_limits: DeveloperKeyUsageLimits,
2529+
usage_limits: Optional[DeveloperKeyUsageLimits],
25292530
):
25302531
self._key_id = key_id
25312532
self._label = label
@@ -2535,6 +2536,8 @@ def __init__(
25352536
self._usage_limits = usage_limits
25362537

25372538
def __str__(self) -> str:
2539+
if self.label is None:
2540+
return f"DeveloperKey ({self.key_id})"
25382541
return f'DeveloperKey "{self.label}" ({self.key_id})'
25392542

25402543
@staticmethod
@@ -2543,14 +2546,16 @@ def from_json(json: Any) -> "DeveloperKey":
25432546
json = _as_dict(json)
25442547
return DeveloperKey(
25452548
key_id=_str_or_empty(json, "key_id"),
2546-
label=_str_or_empty(json, "label"),
2549+
label=_str_or_none(json.get("label")),
25472550
creation_time=_parse_utc_timestamp(json.get("creation_time")),
25482551
deactivated_time=_parse_utc_timestamp(
25492552
json.get("deactivated_time")
25502553
),
25512554
is_deactivated=json.get("is_deactivated") is True,
2552-
usage_limits=DeveloperKeyUsageLimits.from_json(
2553-
json.get("usage_limits")
2555+
usage_limits=(
2556+
DeveloperKeyUsageLimits.from_json(json["usage_limits"])
2557+
if isinstance(json.get("usage_limits"), dict)
2558+
else None
25542559
),
25552560
)
25562561

@@ -2560,8 +2565,8 @@ def key_id(self) -> str:
25602565
return self._key_id
25612566

25622567
@property
2563-
def label(self) -> str:
2564-
"""Returns the label of the key."""
2568+
def label(self) -> Optional[str]:
2569+
"""Returns the label of the key, or None if the API sent none."""
25652570
return self._label
25662571

25672572
@property
@@ -2580,8 +2585,9 @@ def is_deactivated(self) -> bool:
25802585
return self._is_deactivated
25812586

25822587
@property
2583-
def usage_limits(self) -> DeveloperKeyUsageLimits:
2584-
"""Returns the usage limits of the key."""
2588+
def usage_limits(self) -> Optional[DeveloperKeyUsageLimits]:
2589+
"""Returns the usage limits of the key, or None if the API sent
2590+
none."""
25852591
return self._usage_limits
25862592

25872593

@@ -2686,15 +2692,15 @@ class KeyUsage:
26862692
"""Usage of a single API key in an admin usage analytics report.
26872693
26882694
:param api_key: Masked API key.
2689-
:param api_key_label: Label of the API key.
2695+
:param api_key_label: Label of the API key, or None if absent.
26902696
:param usage: Usage of the API key.
26912697
:param usage_date: Day of the usage, present when grouped by key and day.
26922698
"""
26932699

26942700
def __init__(
26952701
self,
26962702
api_key: str,
2697-
api_key_label: str,
2703+
api_key_label: Optional[str],
26982704
usage: AdminUsageBreakdown,
26992705
usage_date: Optional[datetime.datetime] = None,
27002706
):
@@ -2709,7 +2715,7 @@ def from_json(json: Any) -> "KeyUsage":
27092715
json = _as_dict(json)
27102716
return KeyUsage(
27112717
api_key=_str_or_empty(json, "api_key"),
2712-
api_key_label=_str_or_empty(json, "api_key_label"),
2718+
api_key_label=_str_or_none(json.get("api_key_label")),
27132719
usage=AdminUsageBreakdown.from_json(json.get("usage")),
27142720
usage_date=_parse_utc_timestamp(json.get("usage_date")),
27152721
)
@@ -2720,8 +2726,8 @@ def api_key(self) -> str:
27202726
return self._api_key
27212727

27222728
@property
2723-
def api_key_label(self) -> str:
2724-
"""Returns the label of the API key."""
2729+
def api_key_label(self) -> Optional[str]:
2730+
"""Returns the label of the API key, or None if the API sent none."""
27252731
return self._api_key_label
27262732

27272733
@property

‎tests/offline/test_admin_client.py‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ def test_response_with_malformed_values_is_parsed_tolerantly(mocked):
271271
assert report.start_date is None
272272
assert report.total_usage.total_characters == 0
273273
assert report.total_usage.speech_to_text_minutes == 1.5
274-
assert [k.api_key_label for k in report.key_usages] == ["kept", ""]
274+
assert [k.api_key_label for k in report.key_usages] == ["kept", None]
275275
assert report.key_and_day_usages == []
276276
assert tags.next_page is None
277277
assert [(u.custom_tag, u.usage_date) for u in tags.usage] == [
@@ -282,11 +282,27 @@ def test_response_with_malformed_values_is_parsed_tolerantly(mocked):
282282
assert keys[0].usage_limits.characters is None
283283
assert keys[1].creation_time is None
284284
assert keys[1].is_deactivated is False
285-
assert keys[1].usage_limits.characters is None
285+
assert keys[1].usage_limits is None
286286
assert keys[2].is_deactivated is False
287287
assert keys[2].usage_limits.characters is None
288288

289289

290+
def test_absent_optional_key_fields_are_none(mocked):
291+
key_json = {
292+
k: v
293+
for k, v in DEVELOPER_KEY_JSON.items()
294+
if k not in ("label", "usage_limits")
295+
}
296+
mocked.get(url("v2/admin/developer-keys"), json=[key_json])
297+
298+
key = make_admin_client().list_developer_keys()[0]
299+
300+
assert key.label is None
301+
assert key.usage_limits is None
302+
assert str(key) == f"DeveloperKey ({KEY_ID})"
303+
assert "None" not in str(key)
304+
305+
290306
def test_create_developer_key(mocked):
291307
mocked.post(
292308
url("v2/admin/developer-keys"),

0 commit comments

Comments
 (0)