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/_models.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Iterator, Mapping
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Any, Generic, TypeVar
|
|
9
|
+
|
|
10
|
+
_SENSITIVE_FIELDS = frozenset(
|
|
11
|
+
{
|
|
12
|
+
"access_token",
|
|
13
|
+
"authorization",
|
|
14
|
+
"code",
|
|
15
|
+
"cookie",
|
|
16
|
+
"custom_key",
|
|
17
|
+
"key",
|
|
18
|
+
"password",
|
|
19
|
+
"refresh_token",
|
|
20
|
+
"temp_token",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _redacted(value: Any, field: str | None = None) -> Any:
|
|
26
|
+
if field is not None and field.lower() in _SENSITIVE_FIELDS and value not in (None, ""):
|
|
27
|
+
return "********"
|
|
28
|
+
if isinstance(value, Mapping):
|
|
29
|
+
return {str(key): _redacted(item, str(key)) for key, item in value.items()}
|
|
30
|
+
if isinstance(value, (list, tuple)):
|
|
31
|
+
return [_redacted(item) for item in value]
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _wrapped(value: Any) -> Any:
|
|
36
|
+
if isinstance(value, Mapping):
|
|
37
|
+
return Resource(value)
|
|
38
|
+
if isinstance(value, list):
|
|
39
|
+
return [_wrapped(item) for item in value]
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Resource(Mapping[str, Any]):
|
|
44
|
+
"""A response object supporting both mapping and attribute access."""
|
|
45
|
+
|
|
46
|
+
__slots__ = ("_data",)
|
|
47
|
+
|
|
48
|
+
def __init__(self, data: Mapping[str, Any]) -> None:
|
|
49
|
+
self._data = dict(data)
|
|
50
|
+
|
|
51
|
+
def __getitem__(self, key: str) -> Any:
|
|
52
|
+
return _wrapped(self._data[key])
|
|
53
|
+
|
|
54
|
+
def __iter__(self) -> Iterator[str]:
|
|
55
|
+
return iter(self._data)
|
|
56
|
+
|
|
57
|
+
def __len__(self) -> int:
|
|
58
|
+
return len(self._data)
|
|
59
|
+
|
|
60
|
+
def __getattr__(self, name: str) -> Any:
|
|
61
|
+
try:
|
|
62
|
+
return self[name]
|
|
63
|
+
except KeyError as error:
|
|
64
|
+
raise AttributeError(name) from error
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
return f"{type(self).__name__}({_redacted(self._data)!r})"
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict[str, Any]:
|
|
70
|
+
"""Return an independent dictionary containing the full response data."""
|
|
71
|
+
return deepcopy(self._data)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class User(Resource):
|
|
75
|
+
"""The authenticated user's profile."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class APIKey(Resource):
|
|
79
|
+
"""A user-owned gateway API key."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Group(Resource):
|
|
83
|
+
"""A group available to the authenticated user."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class UsageRecord(Resource):
|
|
87
|
+
"""One gateway usage-history record."""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PlatformQuota(Resource):
|
|
91
|
+
"""Usage and limit values for one upstream platform."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Subscription(Resource):
|
|
95
|
+
"""A user subscription."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class Announcement(Resource):
|
|
99
|
+
"""A user-visible announcement."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Redemption(Resource):
|
|
103
|
+
"""A redemption result or history item."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class KeyGroupMultiplier:
|
|
108
|
+
"""The group multiplier resolved for one API key."""
|
|
109
|
+
|
|
110
|
+
api_key: APIKey
|
|
111
|
+
group: Group | None
|
|
112
|
+
group_id: int | None
|
|
113
|
+
base_multiplier: float | None
|
|
114
|
+
custom_multiplier: float | None
|
|
115
|
+
effective_multiplier: float | None
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def is_custom(self) -> bool:
|
|
119
|
+
return self.custom_multiplier is not None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
T = TypeVar("T", bound=Resource)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class Page(Generic[T]):
|
|
127
|
+
"""One page returned by a list endpoint."""
|
|
128
|
+
|
|
129
|
+
items: tuple[T, ...]
|
|
130
|
+
total: int
|
|
131
|
+
page: int
|
|
132
|
+
page_size: int
|
|
133
|
+
pages: int
|
|
134
|
+
|
|
135
|
+
def __iter__(self) -> Iterator[T]:
|
|
136
|
+
return iter(self.items)
|
|
137
|
+
|
|
138
|
+
def __len__(self) -> int:
|
|
139
|
+
return len(self.items)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def has_next(self) -> bool:
|
|
143
|
+
return self.page < self.pages
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_data(cls, data: Mapping[str, Any], item_type: type[T]) -> Page[T]:
|
|
147
|
+
raw_items = data.get("items", [])
|
|
148
|
+
if not isinstance(raw_items, list):
|
|
149
|
+
raise TypeError("paginated response items must be a list")
|
|
150
|
+
if any(not isinstance(item, Mapping) for item in raw_items):
|
|
151
|
+
raise TypeError("paginated response items must contain objects")
|
|
152
|
+
return cls(
|
|
153
|
+
items=tuple(item_type(item) for item in raw_items),
|
|
154
|
+
total=int(data.get("total", len(raw_items))),
|
|
155
|
+
page=int(data.get("page", 1)),
|
|
156
|
+
page_size=int(data.get("page_size", len(raw_items) or 1)),
|
|
157
|
+
pages=int(data.get("pages", 1)),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True)
|
|
162
|
+
class Balance:
|
|
163
|
+
"""The user's USD-denominated balance values."""
|
|
164
|
+
|
|
165
|
+
balance: Decimal
|
|
166
|
+
frozen_balance: Decimal
|
|
167
|
+
total_recharged: Decimal
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True)
|
|
171
|
+
class SessionTokens:
|
|
172
|
+
"""The current in-memory dashboard session tokens."""
|
|
173
|
+
|
|
174
|
+
access_token: str | None = field(default=None, repr=False)
|
|
175
|
+
refresh_token: str | None = field(default=None, repr=False)
|
|
176
|
+
expires_at: float | None = None
|
|
177
|
+
token_type: str = "Bearer"
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def authenticated(self) -> bool:
|
|
181
|
+
return bool(self.access_token)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def expires_in(self) -> float | None:
|
|
185
|
+
if self.expires_at is None:
|
|
186
|
+
return None
|
|
187
|
+
return max(0.0, self.expires_at - time.time())
|
|
188
|
+
|
|
189
|
+
def needs_refresh(self, leeway: float = 30.0) -> bool:
|
|
190
|
+
return (
|
|
191
|
+
self.refresh_token is not None
|
|
192
|
+
and self.expires_at is not None
|
|
193
|
+
and self.expires_at <= time.time() + leeway
|
|
194
|
+
)
|