Skip to content

Commit 5461b5c

Browse files
hive-botclaude
andcommitted
fix: return known group_by and aggregate_by of admin reports as enum members
AdminUsageReport.group_by and CustomTagUsageReport.aggregate_by were always the raw string, so comparing them with AdminUsageGroupBy or CustomTagUsageAggregateBy members was never true. Unknown values are still returned as the string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3df8b10 commit 5461b5c

4 files changed

Lines changed: 87 additions & 18 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,8 +1315,10 @@ date raises a `ValueError` before any request is sent. Use
13151315
`group_by` to break the usage down by key (`AdminUsageGroupBy.KEY`, see
13161316
`key_usages`) or by key and day (`AdminUsageGroupBy.KEY_AND_DAY`, see
13171317
`key_and_day_usages`). The lists are empty if the report is not grouped that
1318-
way. The `group_by` and `aggregate_by` of a report are plain strings as the API
1319-
names them, for example `"keyAndDay"`.
1318+
way. The `group_by` of an `AdminUsageReport` and the `aggregate_by` of a
1319+
`CustomTagUsageReport` are `AdminUsageGroupBy` and `CustomTagUsageAggregateBy`
1320+
members for known values; a value this library does not know is returned as
1321+
the string the API sent.
13201322

13211323
```python
13221324
report = admin_client.get_usage_analytics(

‎deepl/api_data.py‎

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2396,6 +2396,24 @@ def _str_or_none(value: Any) -> Optional[str]:
23962396
return value if isinstance(value, str) else None
23972397

23982398

2399+
def _group_by_or_none(value: Any) -> Union[AdminUsageGroupBy, str, None]:
2400+
# The live API answers group_by=key_and_day with "keyAndDay".
2401+
if value == "keyAndDay":
2402+
return AdminUsageGroupBy.KEY_AND_DAY
2403+
return _open_enum(AdminUsageGroupBy, value)
2404+
2405+
2406+
def _open_enum(enum_type: Any, value: Any) -> Any:
2407+
"""Returns the enum member for a known value, an unknown string as is,
2408+
and None for anything else."""
2409+
if not isinstance(value, str):
2410+
return None
2411+
try:
2412+
return enum_type(value)
2413+
except ValueError:
2414+
return value
2415+
2416+
23992417
def _limit_or_none(json: dict, key: str) -> Optional[int]:
24002418
return None if isinstance(json.get(key), bool) else get_int_safe(json, key)
24012419

@@ -2747,7 +2765,8 @@ class AdminUsageReport:
27472765
:param total_usage: Usage totals over the whole date range.
27482766
:param start_date: Start of the report period.
27492767
:param end_date: End of the report period.
2750-
:param group_by: Grouping used for the report, if any.
2768+
:param group_by: Grouping used for the report: an AdminUsageGroupBy for a
2769+
known value, the string for an unknown one, or None.
27512770
:param key_usages: Usage per key, present when grouped by key.
27522771
:param key_and_day_usages: Usage per key and day, present when grouped by
27532772
key and day.
@@ -2758,7 +2777,7 @@ def __init__(
27582777
total_usage: AdminUsageBreakdown,
27592778
start_date: Optional[datetime.datetime],
27602779
end_date: Optional[datetime.datetime],
2761-
group_by: Optional[str] = None,
2780+
group_by: Union[AdminUsageGroupBy, str, None] = None,
27622781
key_usages: Optional[List[KeyUsage]] = None,
27632782
key_and_day_usages: Optional[List[KeyUsage]] = None,
27642783
):
@@ -2779,7 +2798,7 @@ def from_json(json: Any) -> "AdminUsageReport":
27792798
),
27802799
start_date=_parse_utc_timestamp(report.get("start_date")),
27812800
end_date=_parse_utc_timestamp(report.get("end_date")),
2782-
group_by=_str_or_none(report.get("group_by")),
2801+
group_by=_group_by_or_none(report.get("group_by")),
27832802
key_usages=[
27842803
KeyUsage.from_json(item)
27852804
for item in _dict_items(report.get("key_usages"))
@@ -2806,9 +2825,9 @@ def end_date(self) -> Optional[datetime.datetime]:
28062825
return self._end_date
28072826

28082827
@property
2809-
def group_by(self) -> Optional[str]:
2810-
"""Returns the grouping used for the report as the API returns it
2811-
(currently "key" or "keyAndDay"), or None."""
2828+
def group_by(self) -> Union[AdminUsageGroupBy, str, None]:
2829+
"""Returns the grouping used for the report as an AdminUsageGroupBy,
2830+
the string the API sent if it is not a known value, or None."""
28122831
return self._group_by
28132832

28142833
@property
@@ -2932,7 +2951,9 @@ def usage_date(self) -> Optional[datetime.datetime]:
29322951
class CustomTagUsageReport:
29332952
"""Admin custom tag usage analytics report for a date range.
29342953
2935-
:param aggregate_by: Aggregation used for the report.
2954+
:param aggregate_by: Aggregation used for the report: a
2955+
CustomTagUsageAggregateBy for a known value, the string for an
2956+
unknown one, or None.
29362957
:param start_date: Start of the report period.
29372958
:param end_date: End of the report period.
29382959
:param next_page: Page number to request the next page, or None if this
@@ -2942,7 +2963,7 @@ class CustomTagUsageReport:
29422963

29432964
def __init__(
29442965
self,
2945-
aggregate_by: Optional[str],
2966+
aggregate_by: Union[CustomTagUsageAggregateBy, str, None],
29462967
start_date: Optional[datetime.datetime],
29472968
end_date: Optional[datetime.datetime],
29482969
next_page: Optional[int],
@@ -2959,7 +2980,9 @@ def from_json(json: Any) -> "CustomTagUsageReport":
29592980
"""Create CustomTagUsageReport from the given API JSON object."""
29602981
report = _as_dict(_as_dict(json).get("custom_tag_usage_report"))
29612982
return CustomTagUsageReport(
2962-
aggregate_by=_str_or_none(report.get("aggregate_by")),
2983+
aggregate_by=_open_enum(
2984+
CustomTagUsageAggregateBy, report.get("aggregate_by")
2985+
),
29632986
start_date=_parse_utc_timestamp(report.get("start_date")),
29642987
end_date=_parse_utc_timestamp(report.get("end_date")),
29652988
next_page=get_int_safe(report, "next_page"),
@@ -2970,9 +2993,10 @@ def from_json(json: Any) -> "CustomTagUsageReport":
29702993
)
29712994

29722995
@property
2973-
def aggregate_by(self) -> Optional[str]:
2974-
"""Returns the aggregation used for the report as the API returns it
2975-
(e.g. "period" or "day"), or None."""
2996+
def aggregate_by(self) -> Union[CustomTagUsageAggregateBy, str, None]:
2997+
"""Returns the aggregation used for the report as a
2998+
CustomTagUsageAggregateBy, the string the API sent if it is not a
2999+
known value, or None."""
29763000
return self._aggregate_by
29773001

29783002
@property

‎tests/offline/test_admin_client.py‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_get_usage_analytics(mocked):
9393

9494
assert report.total_usage.speech_to_text_minutes == 107.46
9595
assert report.total_usage.speech_to_speech_minutes == 3.5
96-
assert report.group_by == "keyAndDay"
96+
assert report.group_by is deepl.AdminUsageGroupBy.KEY_AND_DAY
9797
assert report.start_date == datetime.datetime(2025, 9, 29, tzinfo=UTC)
9898
assert report.key_usages == []
9999
usage = report.key_and_day_usages[0]
@@ -144,7 +144,7 @@ def test_get_custom_tag_usage_analytics(mocked):
144144
page=2,
145145
)
146146

147-
assert report.aggregate_by == "day"
147+
assert report.aggregate_by is deepl.CustomTagUsageAggregateBy.DAY
148148
assert report.next_page == 3
149149
assert report.usage[0].breakdown.speech_to_speech_minutes == 4.2
150150
assert report.usage[0].usage_date == datetime.datetime(
@@ -153,6 +153,49 @@ def test_get_custom_tag_usage_analytics(mocked):
153153
assert report.end_date == datetime.datetime(2026, 5, 15, tzinfo=UTC)
154154

155155

156+
@pytest.mark.parametrize(
157+
"group_by, expected",
158+
[
159+
("key", deepl.AdminUsageGroupBy.KEY),
160+
("key_and_day", deepl.AdminUsageGroupBy.KEY_AND_DAY),
161+
("by_team", "by_team"),
162+
],
163+
)
164+
def test_report_group_by_is_open_enum(mocked, group_by, expected):
165+
mocked.get(
166+
url("v2/admin/analytics"),
167+
json={"usage_report": {"group_by": group_by}},
168+
)
169+
170+
report = make_admin_client().get_usage_analytics(
171+
"2026-05-03", "2026-05-04"
172+
)
173+
174+
assert report.group_by == expected
175+
assert type(report.group_by) is type(expected)
176+
177+
178+
@pytest.mark.parametrize(
179+
"aggregate_by, expected",
180+
[
181+
("period", deepl.CustomTagUsageAggregateBy.PERIOD),
182+
("week", "week"),
183+
],
184+
)
185+
def test_report_aggregate_by_is_open_enum(mocked, aggregate_by, expected):
186+
mocked.get(
187+
url("v2/admin/analytics/custom-tags"),
188+
json={"custom_tag_usage_report": {"aggregate_by": aggregate_by}},
189+
)
190+
191+
report = make_admin_client().get_custom_tag_usage_analytics(
192+
"2026-05-03", "2026-05-04"
193+
)
194+
195+
assert report.aggregate_by == expected
196+
assert type(report.aggregate_by) is type(expected)
197+
198+
156199
def test_uses_utc_date_of_datetime(mocked):
157200
mocked.get(
158201
url("v2/admin/analytics"),

‎tests/real_api/test_admin_client.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def test_get_usage_analytics():
2727
assert report.start_date is not None
2828
assert report.start_date.tzinfo is not None
2929
assert report.end_date is not None
30-
assert report.group_by == "keyAndDay"
30+
assert report.group_by is deepl.AdminUsageGroupBy.KEY_AND_DAY
3131

3232

3333
def test_get_custom_tag_usage_analytics():
@@ -39,7 +39,7 @@ def test_get_custom_tag_usage_analytics():
3939

4040
assert report.start_date is not None
4141
assert report.end_date is not None
42-
assert report.aggregate_by == "day"
42+
assert report.aggregate_by is deepl.CustomTagUsageAggregateBy.DAY
4343
assert isinstance(report.usage, list)
4444

4545

0 commit comments

Comments
 (0)