newapi-python 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.
- newapi/__init__.py +75 -0
- newapi/_client.py +620 -0
- newapi/_crypto.py +100 -0
- newapi/_exceptions.py +76 -0
- newapi/_models.py +244 -0
- newapi/_resources.py +775 -0
- newapi/py.typed +0 -0
- newapi_python-0.1.0.dist-info/METADATA +267 -0
- newapi_python-0.1.0.dist-info/RECORD +11 -0
- newapi_python-0.1.0.dist-info/WHEEL +4 -0
- newapi_python-0.1.0.dist-info/licenses/LICENSE +21 -0
newapi/_resources.py
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from decimal import Decimal, InvalidOperation
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
|
8
|
+
|
|
9
|
+
from ._exceptions import ProtocolError
|
|
10
|
+
from ._models import (
|
|
11
|
+
CheckinResult,
|
|
12
|
+
CheckinStatus,
|
|
13
|
+
Group,
|
|
14
|
+
Log,
|
|
15
|
+
LoginSession,
|
|
16
|
+
Page,
|
|
17
|
+
PaymentConfig,
|
|
18
|
+
PaymentLink,
|
|
19
|
+
Redemption,
|
|
20
|
+
Resource,
|
|
21
|
+
Subscription,
|
|
22
|
+
SubscriptionPlan,
|
|
23
|
+
Token,
|
|
24
|
+
TopUpOrder,
|
|
25
|
+
User,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from ._client import NewAPI
|
|
30
|
+
|
|
31
|
+
R = TypeVar("R", bound=Resource)
|
|
32
|
+
_UNSET = object()
|
|
33
|
+
|
|
34
|
+
LOG_TYPES: dict[str, int] = {
|
|
35
|
+
"unknown": 0,
|
|
36
|
+
"topup": 1,
|
|
37
|
+
"consume": 2,
|
|
38
|
+
"manage": 3,
|
|
39
|
+
"system": 4,
|
|
40
|
+
"error": 5,
|
|
41
|
+
"refund": 6,
|
|
42
|
+
"login": 7,
|
|
43
|
+
}
|
|
44
|
+
TOKEN_STATUS = {"unknown": 0, "enabled": 1, "disabled": 2, "expired": 3, "exhausted": 4}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _mapping(data: Any, operation: str) -> Mapping[str, Any]:
|
|
48
|
+
if not isinstance(data, Mapping):
|
|
49
|
+
raise ProtocolError(f"{operation} returned {type(data).__name__}, expected an object")
|
|
50
|
+
return data
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _list(data: Any, operation: str) -> list[Any]:
|
|
54
|
+
if not isinstance(data, list):
|
|
55
|
+
raise ProtocolError(f"{operation} returned {type(data).__name__}, expected a list")
|
|
56
|
+
return data
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _page(data: Any, item_type: type[R], operation: str) -> Page[R]:
|
|
60
|
+
payload = _mapping(data, operation)
|
|
61
|
+
try:
|
|
62
|
+
return Page.from_data(payload, item_type)
|
|
63
|
+
except (TypeError, ValueError) as error:
|
|
64
|
+
raise ProtocolError(f"{operation} returned invalid pagination data") from error
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _positive(value: int, name: str) -> int:
|
|
68
|
+
if isinstance(value, bool) or value < 1:
|
|
69
|
+
raise ValueError(f"{name} must be at least 1")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _nonnegative(value: int | float, name: str) -> int | float:
|
|
74
|
+
if isinstance(value, bool) or not math.isfinite(value) or value < 0:
|
|
75
|
+
raise ValueError(f"{name} must be a finite non-negative number")
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _log_type(value: int | str | None) -> int | None:
|
|
80
|
+
if value is None:
|
|
81
|
+
return None
|
|
82
|
+
if isinstance(value, bool):
|
|
83
|
+
raise ValueError("log_type must be an integer or one of: " + ", ".join(LOG_TYPES))
|
|
84
|
+
if isinstance(value, int):
|
|
85
|
+
return value
|
|
86
|
+
try:
|
|
87
|
+
return LOG_TYPES[value.strip().lower()]
|
|
88
|
+
except KeyError as error:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"log_type must be an integer or one of: " + ", ".join(LOG_TYPES)
|
|
91
|
+
) from error
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _timestamp(value: datetime | int | None, name: str) -> int | None:
|
|
95
|
+
if value is None:
|
|
96
|
+
return None
|
|
97
|
+
if isinstance(value, bool):
|
|
98
|
+
raise ValueError(f"{name} must be a datetime or a Unix timestamp")
|
|
99
|
+
if isinstance(value, int):
|
|
100
|
+
return value
|
|
101
|
+
return int(value.timestamp())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _expiry(value: datetime | int | str) -> int:
|
|
105
|
+
if isinstance(value, bool):
|
|
106
|
+
raise ValueError("expired_time must be a datetime, a Unix timestamp, or -1")
|
|
107
|
+
if isinstance(value, int):
|
|
108
|
+
return value
|
|
109
|
+
if isinstance(value, datetime):
|
|
110
|
+
return int(value.timestamp())
|
|
111
|
+
value = value.strip()
|
|
112
|
+
if value in {"", "-1", "never"}:
|
|
113
|
+
return -1
|
|
114
|
+
try:
|
|
115
|
+
return int(value)
|
|
116
|
+
except ValueError as error:
|
|
117
|
+
raise ValueError("expired_time must be a datetime, a Unix timestamp, or -1") from error
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _allow_ips(value: str | Sequence[str] | None) -> str | None:
|
|
121
|
+
if value is None:
|
|
122
|
+
return None
|
|
123
|
+
if isinstance(value, str):
|
|
124
|
+
return value
|
|
125
|
+
if isinstance(value, (bytes, bytearray)) or any(not isinstance(item, str) for item in value):
|
|
126
|
+
raise ValueError("allow_ips must be a string or a sequence of strings")
|
|
127
|
+
return ",".join(value)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _string_list(value: Sequence[str], name: str) -> list[str]:
|
|
131
|
+
if isinstance(value, (str, bytes)) or any(not isinstance(item, str) for item in value):
|
|
132
|
+
raise ValueError(f"{name} must be a sequence of strings")
|
|
133
|
+
return list(value)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class AccountResource:
|
|
137
|
+
def __init__(self, client: NewAPI) -> None:
|
|
138
|
+
self._client = client
|
|
139
|
+
|
|
140
|
+
def profile(self) -> User:
|
|
141
|
+
return User(_mapping(self._client.request("GET", "user/self"), "profile"))
|
|
142
|
+
|
|
143
|
+
def update_self(self, fields: Mapping[str, Any]) -> None:
|
|
144
|
+
if not _mapping(fields, "update_self fields"):
|
|
145
|
+
raise ValueError("fields must not be empty")
|
|
146
|
+
self._client.request("PUT", "user/self", json=dict(fields))
|
|
147
|
+
|
|
148
|
+
def generate_access_token(self) -> str:
|
|
149
|
+
data = self._client.request("GET", "user/token")
|
|
150
|
+
if not isinstance(data, str) or not data.strip():
|
|
151
|
+
raise ProtocolError("access token generation did not return a key")
|
|
152
|
+
return data
|
|
153
|
+
|
|
154
|
+
def sessions(self) -> tuple[LoginSession, ...]:
|
|
155
|
+
data = _list(self._client.request("GET", "user/sessions"), "login sessions")
|
|
156
|
+
return tuple(LoginSession(item) for item in data if isinstance(item, Mapping))
|
|
157
|
+
|
|
158
|
+
def revoke_session(self, sid: str) -> None:
|
|
159
|
+
if not sid.strip():
|
|
160
|
+
raise ValueError("sid must not be empty")
|
|
161
|
+
self._client.request("DELETE", f"user/sessions/{sid}")
|
|
162
|
+
|
|
163
|
+
def revoke_other_sessions(self) -> int:
|
|
164
|
+
data = _mapping(
|
|
165
|
+
self._client.request("POST", "user/sessions/revoke-others"),
|
|
166
|
+
"session revocation",
|
|
167
|
+
)
|
|
168
|
+
return int(data.get("revoked_count", 0))
|
|
169
|
+
|
|
170
|
+
def aff_code(self) -> str:
|
|
171
|
+
data = self._client.request("GET", "user/aff")
|
|
172
|
+
if not isinstance(data, str) or not data.strip():
|
|
173
|
+
raise ProtocolError("affiliate code response did not return a code")
|
|
174
|
+
return data
|
|
175
|
+
|
|
176
|
+
def transfer_aff_quota(self, quota: int) -> None:
|
|
177
|
+
quota = int(_nonnegative(quota, "quota"))
|
|
178
|
+
if quota == 0:
|
|
179
|
+
raise ValueError("quota must be greater than zero")
|
|
180
|
+
self._client.request("POST", "user/aff_transfer", json={"quota": quota})
|
|
181
|
+
|
|
182
|
+
def checkin_status(self, month: str | None = None) -> CheckinStatus:
|
|
183
|
+
params = {"month": month}
|
|
184
|
+
data = _mapping(
|
|
185
|
+
self._client.request("GET", "user/checkin", params=params),
|
|
186
|
+
"check-in status",
|
|
187
|
+
)
|
|
188
|
+
return CheckinStatus(data)
|
|
189
|
+
|
|
190
|
+
def checkin(self, *, turnstile_token: str | None = None) -> CheckinResult:
|
|
191
|
+
params = {"turnstile": turnstile_token} if turnstile_token else None
|
|
192
|
+
data = _mapping(self._client.request("POST", "user/checkin", params=params), "check-in")
|
|
193
|
+
return CheckinResult(data)
|
|
194
|
+
|
|
195
|
+
def models(self, *, group: str | None = None) -> tuple[str, ...]:
|
|
196
|
+
params = {"group": group}
|
|
197
|
+
data = _list(
|
|
198
|
+
self._client.request("GET", "user/self/models", params=params),
|
|
199
|
+
"user models",
|
|
200
|
+
)
|
|
201
|
+
return tuple(item for item in data if isinstance(item, str))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class TokensResource:
|
|
205
|
+
def __init__(self, client: NewAPI) -> None:
|
|
206
|
+
self._client = client
|
|
207
|
+
|
|
208
|
+
def __call__(self, **kwargs: Any) -> Page[Token]:
|
|
209
|
+
return self.list(**kwargs)
|
|
210
|
+
|
|
211
|
+
def list(
|
|
212
|
+
self,
|
|
213
|
+
*,
|
|
214
|
+
page: int = 1,
|
|
215
|
+
page_size: int = 20,
|
|
216
|
+
) -> Page[Token]:
|
|
217
|
+
params = {
|
|
218
|
+
"p": _positive(page, "page"),
|
|
219
|
+
"page_size": _positive(page_size, "page_size"),
|
|
220
|
+
}
|
|
221
|
+
return _page(self._client.request("GET", "token/", params=params), Token, "tokens")
|
|
222
|
+
|
|
223
|
+
def iter(self, **kwargs: Any) -> Iterator[Token]:
|
|
224
|
+
page_number = int(kwargs.pop("page", 1))
|
|
225
|
+
while True:
|
|
226
|
+
result = self.list(page=page_number, **kwargs)
|
|
227
|
+
yield from result
|
|
228
|
+
if not result.has_next:
|
|
229
|
+
return
|
|
230
|
+
page_number += 1
|
|
231
|
+
|
|
232
|
+
def all(self, *, page_size: int = 100, **filters: Any) -> tuple[Token, ...]:
|
|
233
|
+
return tuple(self.iter(page_size=page_size, **filters))
|
|
234
|
+
|
|
235
|
+
def search(
|
|
236
|
+
self,
|
|
237
|
+
*,
|
|
238
|
+
keyword: str | None = None,
|
|
239
|
+
token: str | None = None,
|
|
240
|
+
page: int = 1,
|
|
241
|
+
page_size: int = 20,
|
|
242
|
+
) -> Page[Token]:
|
|
243
|
+
params = {
|
|
244
|
+
"keyword": keyword,
|
|
245
|
+
"token": token,
|
|
246
|
+
"p": _positive(page, "page"),
|
|
247
|
+
"page_size": _positive(page_size, "page_size"),
|
|
248
|
+
}
|
|
249
|
+
return _page(self._client.request("GET", "token/search", params=params), Token, "tokens")
|
|
250
|
+
|
|
251
|
+
def get(self, token_id: int) -> Token:
|
|
252
|
+
token_id = _positive(token_id, "token_id")
|
|
253
|
+
return Token(_mapping(self._client.request("GET", f"token/{token_id}"), "token"))
|
|
254
|
+
|
|
255
|
+
def reveal(self, token_id: int) -> str:
|
|
256
|
+
token_id = _positive(token_id, "token_id")
|
|
257
|
+
data = _mapping(
|
|
258
|
+
self._client.request("POST", f"token/{token_id}/key"),
|
|
259
|
+
"token key",
|
|
260
|
+
)
|
|
261
|
+
key = data.get("key")
|
|
262
|
+
if not isinstance(key, str) or not key:
|
|
263
|
+
raise ProtocolError("token key response did not contain a key")
|
|
264
|
+
return key
|
|
265
|
+
|
|
266
|
+
def reveal_batch(self, token_ids: Sequence[int]) -> dict[int, str]:
|
|
267
|
+
ids = [_positive(token_id, "token_id") for token_id in token_ids]
|
|
268
|
+
if not ids:
|
|
269
|
+
return {}
|
|
270
|
+
if len(ids) > 100:
|
|
271
|
+
raise ValueError("at most 100 token IDs can be revealed per batch")
|
|
272
|
+
data = _mapping(
|
|
273
|
+
self._client.request("POST", "token/batch/keys", json={"ids": ids}),
|
|
274
|
+
"token keys",
|
|
275
|
+
)
|
|
276
|
+
keys = _mapping(data.get("keys", {}), "token keys")
|
|
277
|
+
try:
|
|
278
|
+
return {int(key): str(value) for key, value in keys.items()}
|
|
279
|
+
except (TypeError, ValueError) as error:
|
|
280
|
+
raise ProtocolError("token keys contained invalid entries") from error
|
|
281
|
+
|
|
282
|
+
def create(
|
|
283
|
+
self,
|
|
284
|
+
name: str,
|
|
285
|
+
*,
|
|
286
|
+
expired_time: datetime | int = -1,
|
|
287
|
+
remain_quota: int = 500000,
|
|
288
|
+
unlimited_quota: bool = True,
|
|
289
|
+
model_limits_enabled: bool = False,
|
|
290
|
+
model_limits: str = "",
|
|
291
|
+
allow_ips: str | Sequence[str] | None = None,
|
|
292
|
+
group: str = "",
|
|
293
|
+
auto_groups: Sequence[str] | None = None,
|
|
294
|
+
cross_group_retry: bool = False,
|
|
295
|
+
) -> None:
|
|
296
|
+
if not name.strip():
|
|
297
|
+
raise ValueError("name must not be empty")
|
|
298
|
+
if len(name) > 50:
|
|
299
|
+
raise ValueError("name must be at most 50 characters")
|
|
300
|
+
remain_quota = int(_nonnegative(remain_quota, "remain_quota"))
|
|
301
|
+
payload: dict[str, Any] = {
|
|
302
|
+
"name": name,
|
|
303
|
+
"expired_time": _expiry(expired_time),
|
|
304
|
+
"remain_quota": remain_quota,
|
|
305
|
+
"unlimited_quota": unlimited_quota,
|
|
306
|
+
"model_limits_enabled": model_limits_enabled,
|
|
307
|
+
"model_limits": model_limits,
|
|
308
|
+
"allow_ips": _allow_ips(allow_ips) if allow_ips is not None else "",
|
|
309
|
+
"group": group,
|
|
310
|
+
"cross_group_retry": cross_group_retry,
|
|
311
|
+
}
|
|
312
|
+
if auto_groups is not None:
|
|
313
|
+
payload["auto_groups"] = _string_list(auto_groups, "auto_groups")
|
|
314
|
+
self._client.request("POST", "token/", json=payload)
|
|
315
|
+
|
|
316
|
+
def create_and_reveal(
|
|
317
|
+
self,
|
|
318
|
+
name: str,
|
|
319
|
+
*,
|
|
320
|
+
expired_time: datetime | int = -1,
|
|
321
|
+
remain_quota: int = 500000,
|
|
322
|
+
unlimited_quota: bool = True,
|
|
323
|
+
model_limits_enabled: bool = False,
|
|
324
|
+
model_limits: str = "",
|
|
325
|
+
allow_ips: str | Sequence[str] | None = None,
|
|
326
|
+
group: str = "",
|
|
327
|
+
auto_groups: Sequence[str] | None = None,
|
|
328
|
+
cross_group_retry: bool = False,
|
|
329
|
+
) -> tuple[Token, str]:
|
|
330
|
+
self.create(
|
|
331
|
+
name,
|
|
332
|
+
expired_time=expired_time,
|
|
333
|
+
remain_quota=remain_quota,
|
|
334
|
+
unlimited_quota=unlimited_quota,
|
|
335
|
+
model_limits_enabled=model_limits_enabled,
|
|
336
|
+
model_limits=model_limits,
|
|
337
|
+
allow_ips=allow_ips,
|
|
338
|
+
group=group,
|
|
339
|
+
auto_groups=auto_groups,
|
|
340
|
+
cross_group_retry=cross_group_retry,
|
|
341
|
+
)
|
|
342
|
+
for token in self.iter(page_size=100):
|
|
343
|
+
if token.get("name") == name:
|
|
344
|
+
return token, self.reveal(int(token["id"]))
|
|
345
|
+
raise ProtocolError("created token could not be found in the token list")
|
|
346
|
+
|
|
347
|
+
def update(
|
|
348
|
+
self,
|
|
349
|
+
token_id: int,
|
|
350
|
+
*,
|
|
351
|
+
name: Any = _UNSET,
|
|
352
|
+
expired_time: Any = _UNSET,
|
|
353
|
+
remain_quota: Any = _UNSET,
|
|
354
|
+
unlimited_quota: Any = _UNSET,
|
|
355
|
+
model_limits_enabled: Any = _UNSET,
|
|
356
|
+
model_limits: Any = _UNSET,
|
|
357
|
+
allow_ips: Any = _UNSET,
|
|
358
|
+
group: Any = _UNSET,
|
|
359
|
+
auto_groups: Any = _UNSET,
|
|
360
|
+
cross_group_retry: Any = _UNSET,
|
|
361
|
+
fetch_current: bool = True,
|
|
362
|
+
) -> Token:
|
|
363
|
+
token_id = _positive(token_id, "token_id")
|
|
364
|
+
current: Token | None = None
|
|
365
|
+
if fetch_current:
|
|
366
|
+
current = self.get(token_id)
|
|
367
|
+
payload: dict[str, Any] = {"id": token_id}
|
|
368
|
+
|
|
369
|
+
def value_or_current(field: str, provided: Any, default: Any) -> Any:
|
|
370
|
+
if provided is not _UNSET:
|
|
371
|
+
return provided
|
|
372
|
+
if current is not None and current.get(field) is not None:
|
|
373
|
+
return current[field]
|
|
374
|
+
return default
|
|
375
|
+
|
|
376
|
+
raw_name = value_or_current("name", name, "")
|
|
377
|
+
if not isinstance(raw_name, str) or not raw_name.strip():
|
|
378
|
+
raise ValueError("name must not be empty")
|
|
379
|
+
if len(raw_name) > 50:
|
|
380
|
+
raise ValueError("name must be at most 50 characters")
|
|
381
|
+
payload["name"] = raw_name
|
|
382
|
+
raw_expired = value_or_current("expired_time", expired_time, -1)
|
|
383
|
+
if expired_time is not _UNSET:
|
|
384
|
+
payload["expired_time"] = _expiry(expired_time)
|
|
385
|
+
else:
|
|
386
|
+
payload["expired_time"] = int(raw_expired) if raw_expired is not None else -1
|
|
387
|
+
raw_quota = value_or_current("remain_quota", remain_quota, 0)
|
|
388
|
+
if remain_quota is not _UNSET:
|
|
389
|
+
payload["remain_quota"] = int(_nonnegative(remain_quota, "remain_quota"))
|
|
390
|
+
else:
|
|
391
|
+
payload["remain_quota"] = int(raw_quota) if raw_quota is not None else 0
|
|
392
|
+
payload["unlimited_quota"] = bool(
|
|
393
|
+
value_or_current("unlimited_quota", unlimited_quota, False)
|
|
394
|
+
)
|
|
395
|
+
payload["model_limits_enabled"] = bool(
|
|
396
|
+
value_or_current("model_limits_enabled", model_limits_enabled, False)
|
|
397
|
+
)
|
|
398
|
+
payload["model_limits"] = str(value_or_current("model_limits", model_limits, ""))
|
|
399
|
+
raw_ips = value_or_current("allow_ips", allow_ips, "")
|
|
400
|
+
if allow_ips is not _UNSET:
|
|
401
|
+
payload["allow_ips"] = _allow_ips(allow_ips) if allow_ips is not None else ""
|
|
402
|
+
else:
|
|
403
|
+
payload["allow_ips"] = str(raw_ips) if raw_ips is not None else ""
|
|
404
|
+
payload["group"] = str(value_or_current("group", group, ""))
|
|
405
|
+
if auto_groups is not _UNSET:
|
|
406
|
+
if auto_groups is None:
|
|
407
|
+
payload["auto_groups"] = None
|
|
408
|
+
else:
|
|
409
|
+
payload["auto_groups"] = _string_list(auto_groups, "auto_groups")
|
|
410
|
+
elif current is not None:
|
|
411
|
+
payload["auto_groups"] = current.get("auto_groups")
|
|
412
|
+
payload["cross_group_retry"] = bool(
|
|
413
|
+
value_or_current("cross_group_retry", cross_group_retry, False)
|
|
414
|
+
)
|
|
415
|
+
data = self._client.request("PUT", "token/", json=payload)
|
|
416
|
+
if data is None:
|
|
417
|
+
return self.get(token_id)
|
|
418
|
+
return Token(_mapping(data, "updated token"))
|
|
419
|
+
|
|
420
|
+
def set_status(
|
|
421
|
+
self,
|
|
422
|
+
token_id: int,
|
|
423
|
+
status: int | Literal["enabled", "disabled", "expired", "exhausted"],
|
|
424
|
+
) -> Token:
|
|
425
|
+
token_id = _positive(token_id, "token_id")
|
|
426
|
+
if isinstance(status, str):
|
|
427
|
+
try:
|
|
428
|
+
status = TOKEN_STATUS[status.strip().lower()]
|
|
429
|
+
except KeyError as error:
|
|
430
|
+
raise ValueError(
|
|
431
|
+
"status must be an integer or one of: " + ", ".join(TOKEN_STATUS)
|
|
432
|
+
) from error
|
|
433
|
+
if isinstance(status, bool) or not isinstance(status, int) or status < 0:
|
|
434
|
+
raise ValueError("status must be an integer or one of: " + ", ".join(TOKEN_STATUS))
|
|
435
|
+
data = self._client.request(
|
|
436
|
+
"PUT",
|
|
437
|
+
"token/",
|
|
438
|
+
params={"status_only": "true"},
|
|
439
|
+
json={"id": token_id, "status": status},
|
|
440
|
+
)
|
|
441
|
+
if data is None:
|
|
442
|
+
return self.get(token_id)
|
|
443
|
+
return Token(_mapping(data, "updated token"))
|
|
444
|
+
|
|
445
|
+
def enable(self, token_id: int) -> Token:
|
|
446
|
+
return self.set_status(token_id, "enabled")
|
|
447
|
+
|
|
448
|
+
def disable(self, token_id: int) -> Token:
|
|
449
|
+
return self.set_status(token_id, "disabled")
|
|
450
|
+
|
|
451
|
+
def delete(self, token_id: int) -> None:
|
|
452
|
+
token_id = _positive(token_id, "token_id")
|
|
453
|
+
self._client.request("DELETE", f"token/{token_id}")
|
|
454
|
+
|
|
455
|
+
def delete_batch(self, token_ids: Sequence[int]) -> int:
|
|
456
|
+
ids = [_positive(token_id, "token_id") for token_id in token_ids]
|
|
457
|
+
if not ids:
|
|
458
|
+
raise ValueError("token_ids must not be empty")
|
|
459
|
+
data = self._client.request("POST", "token/batch", json={"ids": ids})
|
|
460
|
+
return int(data) if isinstance(data, int) else 0
|
|
461
|
+
|
|
462
|
+
def auto_groups(self) -> Resource:
|
|
463
|
+
data = _mapping(self._client.request("GET", "token/auto-groups"), "auto groups")
|
|
464
|
+
return Resource(data)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class LogsResource:
|
|
468
|
+
def __init__(self, client: NewAPI) -> None:
|
|
469
|
+
self._client = client
|
|
470
|
+
|
|
471
|
+
def __call__(self, **kwargs: Any) -> Page[Log]:
|
|
472
|
+
return self.list(**kwargs)
|
|
473
|
+
|
|
474
|
+
def list(
|
|
475
|
+
self,
|
|
476
|
+
*,
|
|
477
|
+
page: int = 1,
|
|
478
|
+
page_size: int = 20,
|
|
479
|
+
log_type: int | str | None = None,
|
|
480
|
+
start_timestamp: datetime | int | None = None,
|
|
481
|
+
end_timestamp: datetime | int | None = None,
|
|
482
|
+
token_name: str | None = None,
|
|
483
|
+
model_name: str | None = None,
|
|
484
|
+
group: str | None = None,
|
|
485
|
+
request_id: str | None = None,
|
|
486
|
+
upstream_request_id: str | None = None,
|
|
487
|
+
) -> Page[Log]:
|
|
488
|
+
params = {
|
|
489
|
+
"p": _positive(page, "page"),
|
|
490
|
+
"page_size": _positive(page_size, "page_size"),
|
|
491
|
+
"type": _log_type(log_type),
|
|
492
|
+
"start_timestamp": _timestamp(start_timestamp, "start_timestamp"),
|
|
493
|
+
"end_timestamp": _timestamp(end_timestamp, "end_timestamp"),
|
|
494
|
+
"token_name": token_name,
|
|
495
|
+
"model_name": model_name,
|
|
496
|
+
"group": group,
|
|
497
|
+
"request_id": request_id,
|
|
498
|
+
"upstream_request_id": upstream_request_id,
|
|
499
|
+
}
|
|
500
|
+
return _page(self._client.request("GET", "log/self", params=params), Log, "logs")
|
|
501
|
+
|
|
502
|
+
def iter(self, **kwargs: Any) -> Iterator[Log]:
|
|
503
|
+
page_number = int(kwargs.pop("page", 1))
|
|
504
|
+
while True:
|
|
505
|
+
result = self.list(page=page_number, **kwargs)
|
|
506
|
+
yield from result
|
|
507
|
+
if not result.has_next:
|
|
508
|
+
return
|
|
509
|
+
page_number += 1
|
|
510
|
+
|
|
511
|
+
def all(self, *, page_size: int = 100, **filters: Any) -> tuple[Log, ...]:
|
|
512
|
+
return tuple(self.iter(page_size=page_size, **filters))
|
|
513
|
+
|
|
514
|
+
def stat(
|
|
515
|
+
self,
|
|
516
|
+
*,
|
|
517
|
+
log_type: int | str | None = None,
|
|
518
|
+
start_timestamp: datetime | int | None = None,
|
|
519
|
+
end_timestamp: datetime | int | None = None,
|
|
520
|
+
token_name: str | None = None,
|
|
521
|
+
model_name: str | None = None,
|
|
522
|
+
group: str | None = None,
|
|
523
|
+
) -> Resource:
|
|
524
|
+
params = {
|
|
525
|
+
"type": _log_type(log_type),
|
|
526
|
+
"start_timestamp": _timestamp(start_timestamp, "start_timestamp"),
|
|
527
|
+
"end_timestamp": _timestamp(end_timestamp, "end_timestamp"),
|
|
528
|
+
"token_name": token_name,
|
|
529
|
+
"model_name": model_name,
|
|
530
|
+
"group": group,
|
|
531
|
+
}
|
|
532
|
+
return Resource(
|
|
533
|
+
_mapping(self._client.request("GET", "log/self/stat", params=params), "log stats")
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
class DashboardResource:
|
|
538
|
+
def __init__(self, client: NewAPI) -> None:
|
|
539
|
+
self._client = client
|
|
540
|
+
|
|
541
|
+
def quota_data(
|
|
542
|
+
self,
|
|
543
|
+
*,
|
|
544
|
+
start_timestamp: datetime | int | None = None,
|
|
545
|
+
end_timestamp: datetime | int | None = None,
|
|
546
|
+
) -> tuple[Resource, ...]:
|
|
547
|
+
params = {
|
|
548
|
+
"start_timestamp": _timestamp(start_timestamp, "start_timestamp"),
|
|
549
|
+
"end_timestamp": _timestamp(end_timestamp, "end_timestamp"),
|
|
550
|
+
}
|
|
551
|
+
data = _list(
|
|
552
|
+
self._client.request("GET", "data/self", params=params),
|
|
553
|
+
"quota data",
|
|
554
|
+
)
|
|
555
|
+
return tuple(Resource(item) for item in data if isinstance(item, Mapping))
|
|
556
|
+
|
|
557
|
+
def flow_data(
|
|
558
|
+
self,
|
|
559
|
+
*,
|
|
560
|
+
start_timestamp: datetime | int,
|
|
561
|
+
end_timestamp: datetime | int,
|
|
562
|
+
) -> tuple[Resource, ...]:
|
|
563
|
+
params = {
|
|
564
|
+
"start_timestamp": _timestamp(start_timestamp, "start_timestamp"),
|
|
565
|
+
"end_timestamp": _timestamp(end_timestamp, "end_timestamp"),
|
|
566
|
+
}
|
|
567
|
+
if not params["start_timestamp"] or not params["end_timestamp"]:
|
|
568
|
+
raise ValueError("start_timestamp and end_timestamp are required and must be positive")
|
|
569
|
+
if params["end_timestamp"] < params["start_timestamp"]:
|
|
570
|
+
raise ValueError("end_timestamp must not be before start_timestamp")
|
|
571
|
+
data = _list(
|
|
572
|
+
self._client.request("GET", "data/flow/self", params=params),
|
|
573
|
+
"flow quota data",
|
|
574
|
+
)
|
|
575
|
+
return tuple(Resource(item) for item in data if isinstance(item, Mapping))
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class GroupsResource:
|
|
579
|
+
def __init__(self, client: NewAPI) -> None:
|
|
580
|
+
self._client = client
|
|
581
|
+
|
|
582
|
+
def __call__(self) -> dict[str, Group]:
|
|
583
|
+
return self.list()
|
|
584
|
+
|
|
585
|
+
def list(self) -> dict[str, Group]:
|
|
586
|
+
data = _mapping(self._client.request("GET", "user/self/groups"), "groups")
|
|
587
|
+
groups: dict[str, Group] = {}
|
|
588
|
+
for name, item in data.items():
|
|
589
|
+
if not isinstance(item, Mapping):
|
|
590
|
+
raise ProtocolError("groups response contained a non-object group")
|
|
591
|
+
fields = dict(item)
|
|
592
|
+
fields["name"] = name
|
|
593
|
+
groups[str(name)] = Group(fields)
|
|
594
|
+
return groups
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class TopUpResource:
|
|
598
|
+
def __init__(self, client: NewAPI) -> None:
|
|
599
|
+
self._client = client
|
|
600
|
+
|
|
601
|
+
def info(self) -> PaymentConfig:
|
|
602
|
+
data = _mapping(self._client.request("GET", "user/topup/info"), "topup info")
|
|
603
|
+
return PaymentConfig(data)
|
|
604
|
+
|
|
605
|
+
def __call__(self, **kwargs: Any) -> Page[TopUpOrder]:
|
|
606
|
+
return self.list(**kwargs)
|
|
607
|
+
|
|
608
|
+
def list(
|
|
609
|
+
self,
|
|
610
|
+
*,
|
|
611
|
+
page: int = 1,
|
|
612
|
+
page_size: int = 20,
|
|
613
|
+
keyword: str | None = None,
|
|
614
|
+
) -> Page[TopUpOrder]:
|
|
615
|
+
params = {
|
|
616
|
+
"p": _positive(page, "page"),
|
|
617
|
+
"page_size": _positive(page_size, "page_size"),
|
|
618
|
+
"keyword": keyword,
|
|
619
|
+
}
|
|
620
|
+
return _page(
|
|
621
|
+
self._client.request("GET", "user/topup/self", params=params),
|
|
622
|
+
TopUpOrder,
|
|
623
|
+
"topup orders",
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
def orders(self, **kwargs: Any) -> Page[TopUpOrder]:
|
|
627
|
+
return self.list(**kwargs)
|
|
628
|
+
|
|
629
|
+
def redeem(self, code: str) -> Redemption:
|
|
630
|
+
if not code.strip():
|
|
631
|
+
raise ValueError("code must not be empty")
|
|
632
|
+
data = self._client.request("POST", "user/topup", json={"key": code})
|
|
633
|
+
return Redemption({"quota": data} if not isinstance(data, Mapping) else data)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
class PaymentResource:
|
|
637
|
+
def __init__(self, client: NewAPI) -> None:
|
|
638
|
+
self._client = client
|
|
639
|
+
|
|
640
|
+
def epay_amount(self, amount: int) -> Decimal:
|
|
641
|
+
amount = int(_nonnegative(amount, "amount"))
|
|
642
|
+
return self._amount("user/amount", {"amount": amount})
|
|
643
|
+
|
|
644
|
+
def epay_pay(self, amount: int, payment_method: str) -> PaymentLink:
|
|
645
|
+
amount = int(_nonnegative(amount, "amount"))
|
|
646
|
+
if not payment_method.strip():
|
|
647
|
+
raise ValueError("payment_method must not be empty")
|
|
648
|
+
payload = {"amount": amount, "payment_method": payment_method}
|
|
649
|
+
data = _mapping(
|
|
650
|
+
self._client.request("POST", "user/pay", json=payload),
|
|
651
|
+
"epay payment",
|
|
652
|
+
)
|
|
653
|
+
url = data.get("url")
|
|
654
|
+
return PaymentLink(
|
|
655
|
+
{
|
|
656
|
+
"url": url if isinstance(url, str) else None,
|
|
657
|
+
"params": data.get("data") if isinstance(data.get("data"), Mapping) else None,
|
|
658
|
+
}
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
def stripe_amount(self, amount: int) -> Decimal:
|
|
662
|
+
amount = int(_nonnegative(amount, "amount"))
|
|
663
|
+
return self._amount("user/stripe/amount", {"amount": amount, "payment_method": "stripe"})
|
|
664
|
+
|
|
665
|
+
def stripe_pay(
|
|
666
|
+
self,
|
|
667
|
+
amount: int,
|
|
668
|
+
*,
|
|
669
|
+
success_url: str | None = None,
|
|
670
|
+
cancel_url: str | None = None,
|
|
671
|
+
) -> str:
|
|
672
|
+
amount = int(_nonnegative(amount, "amount"))
|
|
673
|
+
payload: dict[str, Any] = {"amount": amount, "payment_method": "stripe"}
|
|
674
|
+
if success_url is not None:
|
|
675
|
+
payload["success_url"] = success_url
|
|
676
|
+
if cancel_url is not None:
|
|
677
|
+
payload["cancel_url"] = cancel_url
|
|
678
|
+
data = _mapping(
|
|
679
|
+
self._client.request("POST", "user/stripe/pay", json=payload),
|
|
680
|
+
"stripe payment",
|
|
681
|
+
).get("data")
|
|
682
|
+
if not isinstance(data, Mapping):
|
|
683
|
+
raise ProtocolError("stripe payment did not return a pay link")
|
|
684
|
+
pay_link = data.get("pay_link")
|
|
685
|
+
if not isinstance(pay_link, str) or not pay_link:
|
|
686
|
+
raise ProtocolError("stripe payment did not return a pay link")
|
|
687
|
+
return pay_link
|
|
688
|
+
|
|
689
|
+
def creem_pay(self, product_id: str) -> str:
|
|
690
|
+
if not product_id.strip():
|
|
691
|
+
raise ValueError("product_id must not be empty")
|
|
692
|
+
payload = {"product_id": product_id, "payment_method": "creem"}
|
|
693
|
+
data = _mapping(
|
|
694
|
+
self._client.request("POST", "user/creem/pay", json=payload),
|
|
695
|
+
"creem payment",
|
|
696
|
+
).get("data")
|
|
697
|
+
if not isinstance(data, Mapping):
|
|
698
|
+
raise ProtocolError("creem payment did not return a pay link")
|
|
699
|
+
pay_link = data.get("pay_link")
|
|
700
|
+
if not isinstance(pay_link, str) or not pay_link:
|
|
701
|
+
raise ProtocolError("creem payment did not return a pay link")
|
|
702
|
+
return pay_link
|
|
703
|
+
|
|
704
|
+
def _amount(self, path: str, payload: Mapping[str, Any]) -> Decimal:
|
|
705
|
+
data = _mapping(self._client.request("POST", path, json=dict(payload)), "payment amount")
|
|
706
|
+
value = data.get("data")
|
|
707
|
+
if not isinstance(value, str) or not value:
|
|
708
|
+
raise ProtocolError("payment amount response did not contain an amount")
|
|
709
|
+
try:
|
|
710
|
+
return Decimal(value)
|
|
711
|
+
except InvalidOperation as error:
|
|
712
|
+
raise ProtocolError("payment amount response contained an invalid amount") from error
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class SubscriptionResource:
|
|
716
|
+
def __init__(self, client: NewAPI) -> None:
|
|
717
|
+
self._client = client
|
|
718
|
+
|
|
719
|
+
def __call__(self) -> Resource:
|
|
720
|
+
return self.self()
|
|
721
|
+
|
|
722
|
+
def plans(self) -> tuple[SubscriptionPlan, ...]:
|
|
723
|
+
data = _list(self._client.request("GET", "subscription/plans"), "subscription plans")
|
|
724
|
+
plans: list[SubscriptionPlan] = []
|
|
725
|
+
for item in data:
|
|
726
|
+
if not isinstance(item, Mapping):
|
|
727
|
+
continue
|
|
728
|
+
plan = item.get("plan")
|
|
729
|
+
if isinstance(plan, Mapping):
|
|
730
|
+
plans.append(SubscriptionPlan(plan))
|
|
731
|
+
return tuple(plans)
|
|
732
|
+
|
|
733
|
+
def self(self) -> Resource:
|
|
734
|
+
data = _mapping(self._client.request("GET", "subscription/self"), "subscriptions")
|
|
735
|
+
return Resource(data)
|
|
736
|
+
|
|
737
|
+
def active(self) -> tuple[Subscription, ...]:
|
|
738
|
+
data = _mapping(self.self(), "subscriptions").get("subscriptions", [])
|
|
739
|
+
return tuple(
|
|
740
|
+
Subscription(item) for item in _list(data, "subscriptions") if isinstance(item, Mapping)
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
def set_preference(
|
|
744
|
+
self, preference: Literal["subscription_first", "balance_first", "subscription_only"]
|
|
745
|
+
) -> Resource:
|
|
746
|
+
data = _mapping(
|
|
747
|
+
self._client.request(
|
|
748
|
+
"PUT",
|
|
749
|
+
"subscription/self/preference",
|
|
750
|
+
json={"billing_preference": preference},
|
|
751
|
+
),
|
|
752
|
+
"billing preference",
|
|
753
|
+
)
|
|
754
|
+
return Resource(data)
|
|
755
|
+
|
|
756
|
+
def purchase_with_balance(self, plan_id: int) -> None:
|
|
757
|
+
plan_id = _positive(plan_id, "plan_id")
|
|
758
|
+
self._client.request("POST", "subscription/balance/pay", json={"plan_id": plan_id})
|
|
759
|
+
|
|
760
|
+
def stripe_pay(self, plan_id: int) -> str:
|
|
761
|
+
plan_id = _positive(plan_id, "plan_id")
|
|
762
|
+
data = _mapping(
|
|
763
|
+
self._client.request(
|
|
764
|
+
"POST",
|
|
765
|
+
"subscription/stripe/pay",
|
|
766
|
+
json={"plan_id": plan_id},
|
|
767
|
+
),
|
|
768
|
+
"subscription stripe payment",
|
|
769
|
+
).get("data")
|
|
770
|
+
if not isinstance(data, Mapping):
|
|
771
|
+
raise ProtocolError("subscription stripe payment did not return a pay link")
|
|
772
|
+
pay_link = data.get("pay_link")
|
|
773
|
+
if not isinstance(pay_link, str) or not pay_link:
|
|
774
|
+
raise ProtocolError("subscription stripe payment did not return a pay link")
|
|
775
|
+
return pay_link
|