sub2api 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sub2api/__init__.py +61 -0
- sub2api/_client.py +486 -0
- sub2api/_exceptions.py +83 -0
- sub2api/_models.py +194 -0
- sub2api/_resources.py +574 -0
- sub2api/py.typed +0 -0
- sub2api-0.1.0.dist-info/METADATA +249 -0
- sub2api-0.1.0.dist-info/RECORD +10 -0
- sub2api-0.1.0.dist-info/WHEEL +4 -0
- sub2api-0.1.0.dist-info/licenses/LICENSE +21 -0
sub2api/_resources.py
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
|
8
|
+
|
|
9
|
+
from ._exceptions import ProtocolError
|
|
10
|
+
from ._models import (
|
|
11
|
+
Announcement,
|
|
12
|
+
APIKey,
|
|
13
|
+
Balance,
|
|
14
|
+
Group,
|
|
15
|
+
KeyGroupMultiplier,
|
|
16
|
+
Page,
|
|
17
|
+
PlatformQuota,
|
|
18
|
+
Redemption,
|
|
19
|
+
Resource,
|
|
20
|
+
Subscription,
|
|
21
|
+
UsageRecord,
|
|
22
|
+
User,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from ._client import Sub2API
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
R = TypeVar("R", bound=Resource)
|
|
30
|
+
_UNSET = object()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _mapping(data: Any, operation: str) -> Mapping[str, Any]:
|
|
34
|
+
if not isinstance(data, Mapping):
|
|
35
|
+
raise ProtocolError(f"{operation} returned {type(data).__name__}, expected an object")
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _list(data: Any, operation: str) -> list[Any]:
|
|
40
|
+
if not isinstance(data, list):
|
|
41
|
+
raise ProtocolError(f"{operation} returned {type(data).__name__}, expected a list")
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _page(data: Any, item_type: type[R], operation: str) -> Page[R]:
|
|
46
|
+
payload = _mapping(data, operation)
|
|
47
|
+
try:
|
|
48
|
+
return Page.from_data(payload, item_type)
|
|
49
|
+
except (TypeError, ValueError) as error:
|
|
50
|
+
raise ProtocolError(f"{operation} returned invalid pagination data") from error
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _positive(value: int, name: str) -> int:
|
|
54
|
+
if isinstance(value, bool) or value < 1:
|
|
55
|
+
raise ValueError(f"{name} must be at least 1")
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _nonnegative(value: int | float, name: str) -> int | float:
|
|
60
|
+
if isinstance(value, bool) or not math.isfinite(value) or value < 0:
|
|
61
|
+
raise ValueError(f"{name} must be a finite non-negative number")
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _date(value: date | str | None) -> str | None:
|
|
66
|
+
if value is None:
|
|
67
|
+
return None
|
|
68
|
+
return value.isoformat() if isinstance(value, date) else value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _datetime(value: datetime | str) -> str:
|
|
72
|
+
return value.isoformat() if isinstance(value, datetime) else value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _string_list(value: Sequence[str], name: str) -> list[str]:
|
|
76
|
+
if isinstance(value, (str, bytes)) or any(not isinstance(item, str) for item in value):
|
|
77
|
+
raise ValueError(f"{name} must be a sequence of strings")
|
|
78
|
+
return list(value)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AccountResource:
|
|
82
|
+
def __init__(self, client: Sub2API) -> None:
|
|
83
|
+
self._client = client
|
|
84
|
+
|
|
85
|
+
def profile(self) -> User:
|
|
86
|
+
return User(_mapping(self._client.request("GET", "auth/me"), "profile"))
|
|
87
|
+
|
|
88
|
+
def balance(self) -> Balance:
|
|
89
|
+
profile = self.profile()
|
|
90
|
+
try:
|
|
91
|
+
return Balance(
|
|
92
|
+
balance=Decimal(str(profile["balance"])),
|
|
93
|
+
frozen_balance=Decimal(str(profile.get("frozen_balance", 0))),
|
|
94
|
+
total_recharged=Decimal(str(profile.get("total_recharged", 0))),
|
|
95
|
+
)
|
|
96
|
+
except (KeyError, ValueError, TypeError) as error:
|
|
97
|
+
raise ProtocolError("profile response did not contain valid balance values") from error
|
|
98
|
+
|
|
99
|
+
def platform_quotas(self) -> tuple[PlatformQuota, ...]:
|
|
100
|
+
data = _mapping(
|
|
101
|
+
self._client.request("GET", "user/platform-quotas"),
|
|
102
|
+
"platform quotas",
|
|
103
|
+
)
|
|
104
|
+
quotas = _list(data.get("platform_quotas", []), "platform quotas")
|
|
105
|
+
return tuple(PlatformQuota(item) for item in quotas if isinstance(item, Mapping))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class KeysResource:
|
|
109
|
+
def __init__(self, client: Sub2API) -> None:
|
|
110
|
+
self._client = client
|
|
111
|
+
|
|
112
|
+
def __call__(self, **kwargs: Any) -> Page[APIKey]:
|
|
113
|
+
return self.list(**kwargs)
|
|
114
|
+
|
|
115
|
+
def list(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
page: int = 1,
|
|
119
|
+
page_size: int = 20,
|
|
120
|
+
search: str | None = None,
|
|
121
|
+
status: str | None = None,
|
|
122
|
+
group_id: int | None = None,
|
|
123
|
+
sort_by: str | None = None,
|
|
124
|
+
sort_order: Literal["asc", "desc"] | None = None,
|
|
125
|
+
) -> Page[APIKey]:
|
|
126
|
+
params: dict[str, Any] = {
|
|
127
|
+
"page": _positive(page, "page"),
|
|
128
|
+
"page_size": _positive(page_size, "page_size"),
|
|
129
|
+
"search": search,
|
|
130
|
+
"status": status,
|
|
131
|
+
"group_id": group_id,
|
|
132
|
+
"sort_by": sort_by,
|
|
133
|
+
"sort_order": sort_order,
|
|
134
|
+
}
|
|
135
|
+
return _page(self._client.request("GET", "keys", params=params), APIKey, "keys")
|
|
136
|
+
|
|
137
|
+
def iter(self, **kwargs: Any) -> Iterator[APIKey]:
|
|
138
|
+
page_number = int(kwargs.pop("page", 1))
|
|
139
|
+
while True:
|
|
140
|
+
result = self.list(page=page_number, **kwargs)
|
|
141
|
+
yield from result
|
|
142
|
+
if not result.has_next:
|
|
143
|
+
return
|
|
144
|
+
page_number += 1
|
|
145
|
+
|
|
146
|
+
def all(self, *, page_size: int = 200, **filters: Any) -> tuple[APIKey, ...]:
|
|
147
|
+
return tuple(self.iter(page_size=page_size, **filters))
|
|
148
|
+
|
|
149
|
+
def with_group_multipliers(
|
|
150
|
+
self,
|
|
151
|
+
*,
|
|
152
|
+
page_size: int = 200,
|
|
153
|
+
**filters: Any,
|
|
154
|
+
) -> tuple[KeyGroupMultiplier, ...]:
|
|
155
|
+
groups: dict[int, Group] = {}
|
|
156
|
+
for available_group in self._client.groups.list():
|
|
157
|
+
try:
|
|
158
|
+
groups[int(available_group["id"])] = available_group
|
|
159
|
+
except (KeyError, TypeError, ValueError) as error:
|
|
160
|
+
raise ProtocolError("group contained an invalid ID") from error
|
|
161
|
+
custom_rates = self._client.groups.rates()
|
|
162
|
+
resolved: list[KeyGroupMultiplier] = []
|
|
163
|
+
for api_key in self.all(page_size=page_size, **filters):
|
|
164
|
+
raw_group_id = api_key.get("group_id")
|
|
165
|
+
try:
|
|
166
|
+
group_id = int(raw_group_id) if raw_group_id is not None else None
|
|
167
|
+
except (TypeError, ValueError) as error:
|
|
168
|
+
raise ProtocolError("API key contained an invalid group ID") from error
|
|
169
|
+
group = groups.get(group_id) if group_id is not None else None
|
|
170
|
+
embedded_group = api_key.get("group")
|
|
171
|
+
if group is None and isinstance(embedded_group, Mapping):
|
|
172
|
+
group = Group(embedded_group)
|
|
173
|
+
base_multiplier: float | None = None
|
|
174
|
+
if group is not None and group.get("rate_multiplier") is not None:
|
|
175
|
+
try:
|
|
176
|
+
base_multiplier = float(group["rate_multiplier"])
|
|
177
|
+
except (TypeError, ValueError) as error:
|
|
178
|
+
raise ProtocolError("group contained an invalid rate multiplier") from error
|
|
179
|
+
custom_multiplier = custom_rates.get(group_id) if group_id is not None else None
|
|
180
|
+
effective_multiplier = (
|
|
181
|
+
custom_multiplier if custom_multiplier is not None else base_multiplier
|
|
182
|
+
)
|
|
183
|
+
resolved.append(
|
|
184
|
+
KeyGroupMultiplier(
|
|
185
|
+
api_key=api_key,
|
|
186
|
+
group=group,
|
|
187
|
+
group_id=group_id,
|
|
188
|
+
base_multiplier=base_multiplier,
|
|
189
|
+
custom_multiplier=custom_multiplier,
|
|
190
|
+
effective_multiplier=effective_multiplier,
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
return tuple(resolved)
|
|
194
|
+
|
|
195
|
+
def multiplier_map(
|
|
196
|
+
self,
|
|
197
|
+
*,
|
|
198
|
+
key_by: Literal["key", "id", "name"] = "key",
|
|
199
|
+
page_size: int = 200,
|
|
200
|
+
**filters: Any,
|
|
201
|
+
) -> dict[str | int, float | None]:
|
|
202
|
+
result: dict[str | int, float | None] = {}
|
|
203
|
+
for item in self.with_group_multipliers(page_size=page_size, **filters):
|
|
204
|
+
value = item.api_key.get(key_by)
|
|
205
|
+
if not isinstance(value, (str, int)) or isinstance(value, bool):
|
|
206
|
+
raise ProtocolError(f"API key did not contain a valid {key_by!r} field")
|
|
207
|
+
if value in result:
|
|
208
|
+
raise ValueError(f"API key {key_by!r} values are not unique: {value!r}")
|
|
209
|
+
result[value] = item.effective_multiplier
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
def get(self, key_id: int) -> APIKey:
|
|
213
|
+
key_id = _positive(key_id, "key_id")
|
|
214
|
+
return APIKey(_mapping(self._client.request("GET", f"keys/{key_id}"), "API key"))
|
|
215
|
+
|
|
216
|
+
def create(
|
|
217
|
+
self,
|
|
218
|
+
name: str,
|
|
219
|
+
*,
|
|
220
|
+
group_id: int | None = None,
|
|
221
|
+
custom_key: str | None = None,
|
|
222
|
+
ip_whitelist: Sequence[str] | None = None,
|
|
223
|
+
ip_blacklist: Sequence[str] | None = None,
|
|
224
|
+
quota: int | float | None = None,
|
|
225
|
+
expires_in_days: int | None = None,
|
|
226
|
+
rate_limit_5h: int | float | None = None,
|
|
227
|
+
rate_limit_1d: int | float | None = None,
|
|
228
|
+
rate_limit_7d: int | float | None = None,
|
|
229
|
+
idempotency_key: str | None = None,
|
|
230
|
+
) -> APIKey:
|
|
231
|
+
if not name.strip():
|
|
232
|
+
raise ValueError("name must not be empty")
|
|
233
|
+
if group_id is not None:
|
|
234
|
+
_positive(group_id, "group_id")
|
|
235
|
+
payload: dict[str, Any] = {"name": name}
|
|
236
|
+
optional = {
|
|
237
|
+
"group_id": group_id,
|
|
238
|
+
"custom_key": custom_key,
|
|
239
|
+
"ip_whitelist": (
|
|
240
|
+
_string_list(ip_whitelist, "ip_whitelist") if ip_whitelist is not None else None
|
|
241
|
+
),
|
|
242
|
+
"ip_blacklist": (
|
|
243
|
+
_string_list(ip_blacklist, "ip_blacklist") if ip_blacklist is not None else None
|
|
244
|
+
),
|
|
245
|
+
"quota": _nonnegative(quota, "quota") if quota is not None else None,
|
|
246
|
+
"expires_in_days": (
|
|
247
|
+
_positive(expires_in_days, "expires_in_days")
|
|
248
|
+
if expires_in_days is not None
|
|
249
|
+
else None
|
|
250
|
+
),
|
|
251
|
+
"rate_limit_5h": (
|
|
252
|
+
_nonnegative(rate_limit_5h, "rate_limit_5h") if rate_limit_5h is not None else None
|
|
253
|
+
),
|
|
254
|
+
"rate_limit_1d": (
|
|
255
|
+
_nonnegative(rate_limit_1d, "rate_limit_1d") if rate_limit_1d is not None else None
|
|
256
|
+
),
|
|
257
|
+
"rate_limit_7d": (
|
|
258
|
+
_nonnegative(rate_limit_7d, "rate_limit_7d") if rate_limit_7d is not None else None
|
|
259
|
+
),
|
|
260
|
+
}
|
|
261
|
+
payload.update({key: value for key, value in optional.items() if value is not None})
|
|
262
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
263
|
+
data = self._client.request("POST", "keys", json=payload, headers=headers)
|
|
264
|
+
return APIKey(_mapping(data, "created API key"))
|
|
265
|
+
|
|
266
|
+
def update(
|
|
267
|
+
self,
|
|
268
|
+
key_id: int,
|
|
269
|
+
*,
|
|
270
|
+
name: Any = _UNSET,
|
|
271
|
+
group_id: Any = _UNSET,
|
|
272
|
+
status: Any = _UNSET,
|
|
273
|
+
ip_whitelist: Any = _UNSET,
|
|
274
|
+
ip_blacklist: Any = _UNSET,
|
|
275
|
+
quota: Any = _UNSET,
|
|
276
|
+
expires_at: Any = _UNSET,
|
|
277
|
+
clear_expiration: bool = False,
|
|
278
|
+
reset_quota: Any = _UNSET,
|
|
279
|
+
rate_limit_5h: Any = _UNSET,
|
|
280
|
+
rate_limit_1d: Any = _UNSET,
|
|
281
|
+
rate_limit_7d: Any = _UNSET,
|
|
282
|
+
reset_rate_limit_usage: Any = _UNSET,
|
|
283
|
+
) -> APIKey:
|
|
284
|
+
key_id = _positive(key_id, "key_id")
|
|
285
|
+
values = {
|
|
286
|
+
"name": name,
|
|
287
|
+
"group_id": group_id,
|
|
288
|
+
"status": status,
|
|
289
|
+
"ip_whitelist": ip_whitelist,
|
|
290
|
+
"ip_blacklist": ip_blacklist,
|
|
291
|
+
"quota": quota,
|
|
292
|
+
"expires_at": expires_at,
|
|
293
|
+
"reset_quota": reset_quota,
|
|
294
|
+
"rate_limit_5h": rate_limit_5h,
|
|
295
|
+
"rate_limit_1d": rate_limit_1d,
|
|
296
|
+
"rate_limit_7d": rate_limit_7d,
|
|
297
|
+
"reset_rate_limit_usage": reset_rate_limit_usage,
|
|
298
|
+
}
|
|
299
|
+
payload = {key: value for key, value in values.items() if value is not _UNSET}
|
|
300
|
+
if clear_expiration:
|
|
301
|
+
if expires_at is not _UNSET:
|
|
302
|
+
raise ValueError("expires_at and clear_expiration cannot be used together")
|
|
303
|
+
payload["expires_at"] = ""
|
|
304
|
+
elif expires_at is not _UNSET and expires_at is not None:
|
|
305
|
+
payload["expires_at"] = _datetime(expires_at)
|
|
306
|
+
if status is not _UNSET and status not in {"active", "inactive"}:
|
|
307
|
+
raise ValueError("status must be 'active' or 'inactive'")
|
|
308
|
+
if name is not _UNSET and (not isinstance(name, str) or not name.strip()):
|
|
309
|
+
raise ValueError("name must not be empty")
|
|
310
|
+
if group_id is not _UNSET and group_id is not None:
|
|
311
|
+
_positive(group_id, "group_id")
|
|
312
|
+
for field in ("quota", "rate_limit_5h", "rate_limit_1d", "rate_limit_7d"):
|
|
313
|
+
value = payload.get(field, _UNSET)
|
|
314
|
+
if value is not _UNSET:
|
|
315
|
+
payload[field] = _nonnegative(value, field)
|
|
316
|
+
for field in ("ip_whitelist", "ip_blacklist"):
|
|
317
|
+
value = payload.get(field, _UNSET)
|
|
318
|
+
if value is not _UNSET and value is not None:
|
|
319
|
+
payload[field] = _string_list(value, field)
|
|
320
|
+
if not payload:
|
|
321
|
+
raise ValueError("at least one update must be provided")
|
|
322
|
+
data = self._client.request("PUT", f"keys/{key_id}", json=payload)
|
|
323
|
+
return APIKey(_mapping(data, "updated API key"))
|
|
324
|
+
|
|
325
|
+
def delete(self, key_id: int) -> None:
|
|
326
|
+
key_id = _positive(key_id, "key_id")
|
|
327
|
+
self._client.request("DELETE", f"keys/{key_id}")
|
|
328
|
+
|
|
329
|
+
def set_status(self, key_id: int, active: bool) -> APIKey:
|
|
330
|
+
return self.update(key_id, status="active" if active else "inactive")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class GroupsResource:
|
|
334
|
+
def __init__(self, client: Sub2API) -> None:
|
|
335
|
+
self._client = client
|
|
336
|
+
|
|
337
|
+
def __call__(self) -> tuple[Group, ...]:
|
|
338
|
+
return self.list()
|
|
339
|
+
|
|
340
|
+
def list(self) -> tuple[Group, ...]:
|
|
341
|
+
data = _list(self._client.request("GET", "groups/available"), "groups")
|
|
342
|
+
return tuple(Group(item) for item in data if isinstance(item, Mapping))
|
|
343
|
+
|
|
344
|
+
def rates(self) -> dict[int, float]:
|
|
345
|
+
data = self._client.request("GET", "groups/rates")
|
|
346
|
+
if data is None:
|
|
347
|
+
return {}
|
|
348
|
+
mapping = _mapping(data, "group rates")
|
|
349
|
+
try:
|
|
350
|
+
return {int(key): float(value) for key, value in mapping.items()}
|
|
351
|
+
except (TypeError, ValueError) as error:
|
|
352
|
+
raise ProtocolError("group rates contained invalid values") from error
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class UsageResource:
|
|
356
|
+
def __init__(self, client: Sub2API) -> None:
|
|
357
|
+
self._client = client
|
|
358
|
+
|
|
359
|
+
def __call__(self, **kwargs: Any) -> Page[UsageRecord]:
|
|
360
|
+
return self.list(**kwargs)
|
|
361
|
+
|
|
362
|
+
def list(
|
|
363
|
+
self,
|
|
364
|
+
*,
|
|
365
|
+
page: int = 1,
|
|
366
|
+
page_size: int = 20,
|
|
367
|
+
api_key_id: int | None = None,
|
|
368
|
+
group_id: int | None = None,
|
|
369
|
+
model: str | None = None,
|
|
370
|
+
request_type: str | None = None,
|
|
371
|
+
stream: bool | None = None,
|
|
372
|
+
start_date: date | str | None = None,
|
|
373
|
+
end_date: date | str | None = None,
|
|
374
|
+
sort_by: str | None = None,
|
|
375
|
+
sort_order: Literal["asc", "desc"] | None = None,
|
|
376
|
+
native_compaction_v2: bool | None = None,
|
|
377
|
+
billing_type: int | None = None,
|
|
378
|
+
billing_mode: str | None = None,
|
|
379
|
+
) -> Page[UsageRecord]:
|
|
380
|
+
params = {
|
|
381
|
+
"page": _positive(page, "page"),
|
|
382
|
+
"page_size": _positive(page_size, "page_size"),
|
|
383
|
+
"api_key_id": api_key_id,
|
|
384
|
+
"group_id": group_id,
|
|
385
|
+
"model": model,
|
|
386
|
+
"request_type": request_type,
|
|
387
|
+
"stream": stream,
|
|
388
|
+
"start_date": _date(start_date),
|
|
389
|
+
"end_date": _date(end_date),
|
|
390
|
+
"sort_by": sort_by,
|
|
391
|
+
"sort_order": sort_order,
|
|
392
|
+
"native_compaction_v2": native_compaction_v2,
|
|
393
|
+
"billing_type": billing_type,
|
|
394
|
+
"billing_mode": billing_mode,
|
|
395
|
+
}
|
|
396
|
+
return _page(self._client.request("GET", "usage", params=params), UsageRecord, "usage")
|
|
397
|
+
|
|
398
|
+
def iter(self, **kwargs: Any) -> Iterator[UsageRecord]:
|
|
399
|
+
page_number = int(kwargs.pop("page", 1))
|
|
400
|
+
while True:
|
|
401
|
+
result = self.list(page=page_number, **kwargs)
|
|
402
|
+
yield from result
|
|
403
|
+
if not result.has_next:
|
|
404
|
+
return
|
|
405
|
+
page_number += 1
|
|
406
|
+
|
|
407
|
+
def get(self, usage_id: int) -> UsageRecord:
|
|
408
|
+
usage_id = _positive(usage_id, "usage_id")
|
|
409
|
+
data = self._client.request("GET", f"usage/{usage_id}")
|
|
410
|
+
return UsageRecord(_mapping(data, "usage record"))
|
|
411
|
+
|
|
412
|
+
def stats(
|
|
413
|
+
self,
|
|
414
|
+
*,
|
|
415
|
+
period: Literal["today", "week", "month"] | None = None,
|
|
416
|
+
start_date: date | str | None = None,
|
|
417
|
+
end_date: date | str | None = None,
|
|
418
|
+
api_key_id: int | None = None,
|
|
419
|
+
group_id: int | None = None,
|
|
420
|
+
model: str | None = None,
|
|
421
|
+
) -> Resource:
|
|
422
|
+
params = {
|
|
423
|
+
"period": period,
|
|
424
|
+
"start_date": _date(start_date),
|
|
425
|
+
"end_date": _date(end_date),
|
|
426
|
+
"api_key_id": api_key_id,
|
|
427
|
+
"group_id": group_id,
|
|
428
|
+
"model": model,
|
|
429
|
+
}
|
|
430
|
+
return Resource(
|
|
431
|
+
_mapping(self._client.request("GET", "usage/stats", params=params), "usage stats")
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def dashboard(self) -> Resource:
|
|
435
|
+
data = self._client.request("GET", "usage/dashboard/stats")
|
|
436
|
+
return Resource(_mapping(data, "dashboard stats"))
|
|
437
|
+
|
|
438
|
+
def trend(
|
|
439
|
+
self,
|
|
440
|
+
*,
|
|
441
|
+
start_date: date | str | None = None,
|
|
442
|
+
end_date: date | str | None = None,
|
|
443
|
+
granularity: Literal["day", "hour"] | None = None,
|
|
444
|
+
api_key_id: int | None = None,
|
|
445
|
+
model: str | None = None,
|
|
446
|
+
group_id: int | None = None,
|
|
447
|
+
) -> Resource:
|
|
448
|
+
params = {
|
|
449
|
+
"start_date": _date(start_date),
|
|
450
|
+
"end_date": _date(end_date),
|
|
451
|
+
"granularity": granularity,
|
|
452
|
+
"api_key_id": api_key_id,
|
|
453
|
+
"model": model,
|
|
454
|
+
"group_id": group_id,
|
|
455
|
+
}
|
|
456
|
+
data = self._client.request("GET", "usage/dashboard/trend", params=params)
|
|
457
|
+
return Resource(_mapping(data, "usage trend"))
|
|
458
|
+
|
|
459
|
+
def models(
|
|
460
|
+
self,
|
|
461
|
+
*,
|
|
462
|
+
start_date: date | str | None = None,
|
|
463
|
+
end_date: date | str | None = None,
|
|
464
|
+
api_key_id: int | None = None,
|
|
465
|
+
model: str | None = None,
|
|
466
|
+
group_id: int | None = None,
|
|
467
|
+
requested_models: bool = False,
|
|
468
|
+
) -> Resource:
|
|
469
|
+
params = {
|
|
470
|
+
"start_date": _date(start_date),
|
|
471
|
+
"end_date": _date(end_date),
|
|
472
|
+
"api_key_id": api_key_id,
|
|
473
|
+
"model": model,
|
|
474
|
+
"group_id": group_id,
|
|
475
|
+
"model_source": "requested" if requested_models else None,
|
|
476
|
+
}
|
|
477
|
+
data = self._client.request("GET", "usage/dashboard/models", params=params)
|
|
478
|
+
return Resource(_mapping(data, "model usage"))
|
|
479
|
+
|
|
480
|
+
def snapshot(
|
|
481
|
+
self,
|
|
482
|
+
*,
|
|
483
|
+
start_date: date | str | None = None,
|
|
484
|
+
end_date: date | str | None = None,
|
|
485
|
+
granularity: Literal["day", "hour"] | None = None,
|
|
486
|
+
include_trend: bool = True,
|
|
487
|
+
include_model_stats: bool = True,
|
|
488
|
+
include_group_stats: bool = True,
|
|
489
|
+
api_key_id: int | None = None,
|
|
490
|
+
model: str | None = None,
|
|
491
|
+
group_id: int | None = None,
|
|
492
|
+
) -> Resource:
|
|
493
|
+
params = {
|
|
494
|
+
"start_date": _date(start_date),
|
|
495
|
+
"end_date": _date(end_date),
|
|
496
|
+
"granularity": granularity,
|
|
497
|
+
"include_trend": include_trend,
|
|
498
|
+
"include_model_stats": include_model_stats,
|
|
499
|
+
"include_group_stats": include_group_stats,
|
|
500
|
+
"api_key_id": api_key_id,
|
|
501
|
+
"model": model,
|
|
502
|
+
"group_id": group_id,
|
|
503
|
+
}
|
|
504
|
+
data = self._client.request("GET", "usage/dashboard/snapshot-v2", params=params)
|
|
505
|
+
return Resource(_mapping(data, "usage snapshot"))
|
|
506
|
+
|
|
507
|
+
def key_totals(self, api_key_ids: Sequence[int]) -> dict[int, Resource]:
|
|
508
|
+
ids = [_positive(key_id, "api_key_id") for key_id in api_key_ids]
|
|
509
|
+
if not ids:
|
|
510
|
+
return {}
|
|
511
|
+
data = _mapping(
|
|
512
|
+
self._client.request(
|
|
513
|
+
"POST",
|
|
514
|
+
"usage/dashboard/api-keys-usage",
|
|
515
|
+
json={"api_key_ids": ids},
|
|
516
|
+
),
|
|
517
|
+
"API key usage totals",
|
|
518
|
+
)
|
|
519
|
+
stats = _mapping(data.get("stats", {}), "API key usage totals")
|
|
520
|
+
try:
|
|
521
|
+
return {
|
|
522
|
+
int(key): Resource(value)
|
|
523
|
+
for key, value in stats.items()
|
|
524
|
+
if isinstance(value, Mapping)
|
|
525
|
+
}
|
|
526
|
+
except ValueError as error:
|
|
527
|
+
raise ProtocolError("API key usage totals contained an invalid key ID") from error
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
class SubscriptionsResource:
|
|
531
|
+
def __init__(self, client: Sub2API) -> None:
|
|
532
|
+
self._client = client
|
|
533
|
+
|
|
534
|
+
def __call__(self, *, active: bool = False) -> tuple[Subscription, ...]:
|
|
535
|
+
return self.list(active=active)
|
|
536
|
+
|
|
537
|
+
def list(self, *, active: bool = False) -> tuple[Subscription, ...]:
|
|
538
|
+
endpoint = "subscriptions/active" if active else "subscriptions"
|
|
539
|
+
data = _list(self._client.request("GET", endpoint), "subscriptions")
|
|
540
|
+
return tuple(Subscription(item) for item in data if isinstance(item, Mapping))
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class AnnouncementsResource:
|
|
544
|
+
def __init__(self, client: Sub2API) -> None:
|
|
545
|
+
self._client = client
|
|
546
|
+
|
|
547
|
+
def __call__(self) -> tuple[Announcement, ...]:
|
|
548
|
+
return self.list()
|
|
549
|
+
|
|
550
|
+
def list(self) -> tuple[Announcement, ...]:
|
|
551
|
+
data = _list(self._client.request("GET", "announcements"), "announcements")
|
|
552
|
+
return tuple(Announcement(item) for item in data if isinstance(item, Mapping))
|
|
553
|
+
|
|
554
|
+
def mark_read(self, announcement_id: int) -> None:
|
|
555
|
+
announcement_id = _positive(announcement_id, "announcement_id")
|
|
556
|
+
self._client.request("POST", f"announcements/{announcement_id}/read")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
class RedeemResource:
|
|
560
|
+
def __init__(self, client: Sub2API) -> None:
|
|
561
|
+
self._client = client
|
|
562
|
+
|
|
563
|
+
def __call__(self, code: str) -> Redemption:
|
|
564
|
+
return self.apply(code)
|
|
565
|
+
|
|
566
|
+
def apply(self, code: str) -> Redemption:
|
|
567
|
+
if not code.strip():
|
|
568
|
+
raise ValueError("code must not be empty")
|
|
569
|
+
data = self._client.request("POST", "redeem", json={"code": code})
|
|
570
|
+
return Redemption(_mapping(data, "redemption"))
|
|
571
|
+
|
|
572
|
+
def history(self) -> tuple[Redemption, ...]:
|
|
573
|
+
data = _list(self._client.request("GET", "redeem/history"), "redemption history")
|
|
574
|
+
return tuple(Redemption(item) for item in data if isinstance(item, Mapping))
|
sub2api/py.typed
ADDED
|
File without changes
|