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/_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