ogu-api 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.
- ogu_api/__init__.py +59 -0
- ogu_api/_http.py +338 -0
- ogu_api/client.py +112 -0
- ogu_api/errors.py +129 -0
- ogu_api/models.py +48 -0
- ogu_api/proxy.py +40 -0
- ogu_api/py.typed +0 -0
- ogu_api/resources/__init__.py +27 -0
- ogu_api/resources/_base.py +56 -0
- ogu_api/resources/credits.py +86 -0
- ogu_api/resources/feed.py +81 -0
- ogu_api/resources/members.py +55 -0
- ogu_api/resources/messages.py +127 -0
- ogu_api/resources/notifications.py +37 -0
- ogu_api/resources/reputation.py +50 -0
- ogu_api/resources/search.py +60 -0
- ogu_api/resources/session.py +45 -0
- ogu_api/resources/threads.py +103 -0
- ogu_api/resources/usercp.py +147 -0
- ogu_api/resources/users.py +61 -0
- ogu_api-0.1.0.dist-info/METADATA +237 -0
- ogu_api-0.1.0.dist-info/RECORD +24 -0
- ogu_api-0.1.0.dist-info/WHEEL +4 -0
- ogu_api-0.1.0.dist-info/licenses/LICENSE +21 -0
ogu_api/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _package_version
|
|
4
|
+
|
|
5
|
+
from ._http import HttpClient, HttpClientConfig
|
|
6
|
+
from .client import OguClient
|
|
7
|
+
from .errors import (
|
|
8
|
+
OguAPIError,
|
|
9
|
+
OguAuthenticationError,
|
|
10
|
+
OguAuthorizationError,
|
|
11
|
+
OguCreditsError,
|
|
12
|
+
OguError,
|
|
13
|
+
OguLoginError,
|
|
14
|
+
OguNetworkError,
|
|
15
|
+
OguNotFoundError,
|
|
16
|
+
OguParseError,
|
|
17
|
+
OguRateLimitError,
|
|
18
|
+
OguReputationError,
|
|
19
|
+
OguServerError,
|
|
20
|
+
OguSessionError,
|
|
21
|
+
OguTimeoutError,
|
|
22
|
+
OguValidationError,
|
|
23
|
+
)
|
|
24
|
+
from .models import ActionResult, Message, RecentTransaction, ReputationPage, UserProfile
|
|
25
|
+
from .proxy import Proxy
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
__version__ = _package_version('ogu-api')
|
|
29
|
+
|
|
30
|
+
except PackageNotFoundError:
|
|
31
|
+
__version__ = '0.0.0+unknown'
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
'__version__',
|
|
35
|
+
'OguClient',
|
|
36
|
+
'HttpClient',
|
|
37
|
+
'HttpClientConfig',
|
|
38
|
+
'Proxy',
|
|
39
|
+
'ActionResult',
|
|
40
|
+
'Message',
|
|
41
|
+
'RecentTransaction',
|
|
42
|
+
'ReputationPage',
|
|
43
|
+
'UserProfile',
|
|
44
|
+
'OguError',
|
|
45
|
+
'OguAPIError',
|
|
46
|
+
'OguAuthenticationError',
|
|
47
|
+
'OguAuthorizationError',
|
|
48
|
+
'OguNotFoundError',
|
|
49
|
+
'OguValidationError',
|
|
50
|
+
'OguRateLimitError',
|
|
51
|
+
'OguServerError',
|
|
52
|
+
'OguNetworkError',
|
|
53
|
+
'OguTimeoutError',
|
|
54
|
+
'OguParseError',
|
|
55
|
+
'OguSessionError',
|
|
56
|
+
'OguLoginError',
|
|
57
|
+
'OguReputationError',
|
|
58
|
+
'OguCreditsError',
|
|
59
|
+
]
|
ogu_api/_http.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Mapping, MutableMapping
|
|
7
|
+
|
|
8
|
+
import tls_client
|
|
9
|
+
import tls_client.response
|
|
10
|
+
|
|
11
|
+
from .errors import (
|
|
12
|
+
OguAPIError,
|
|
13
|
+
OguAuthenticationError,
|
|
14
|
+
OguAuthorizationError,
|
|
15
|
+
OguNetworkError,
|
|
16
|
+
OguNotFoundError,
|
|
17
|
+
OguRateLimitError,
|
|
18
|
+
OguServerError,
|
|
19
|
+
OguTimeoutError,
|
|
20
|
+
OguValidationError,
|
|
21
|
+
)
|
|
22
|
+
from .proxy import Proxy
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
'HttpClientConfig',
|
|
26
|
+
'HttpClient',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
DEFAULT_BASE_URL = 'https://oguser.com'
|
|
34
|
+
|
|
35
|
+
DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
36
|
+
|
|
37
|
+
DEFAULT_RETRIES = 0
|
|
38
|
+
|
|
39
|
+
DEFAULT_RETRY_BACKOFF_SECONDS = 0.5
|
|
40
|
+
|
|
41
|
+
DEFAULT_CLIENT_IDENTIFIER = 'chrome131'
|
|
42
|
+
|
|
43
|
+
DEFAULT_HEADERS: Mapping[str, str] = {
|
|
44
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
45
|
+
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
|
46
|
+
'Accept-Language': 'en-US,en;q=0.5',
|
|
47
|
+
'Connection': 'keep-alive',
|
|
48
|
+
'Host': 'oguser.com',
|
|
49
|
+
'Priority': 'u=0, i',
|
|
50
|
+
'Referer': 'https://oguser.com/',
|
|
51
|
+
'Sec-Fetch-Dest': 'document',
|
|
52
|
+
'Sec-Fetch-Mode': 'navigate',
|
|
53
|
+
'Sec-Fetch-Site': 'same-origin',
|
|
54
|
+
'Sec-Fetch-User': '?1',
|
|
55
|
+
'Sec-GPC': '1',
|
|
56
|
+
'TE': 'trailers',
|
|
57
|
+
'Upgrade-Insecure-Requests': '1',
|
|
58
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
RETRYABLE_STATUS_CODES: frozenset[int] = frozenset({429, 500, 502, 503, 504})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen = True)
|
|
65
|
+
class HttpClientConfig:
|
|
66
|
+
base_url: str = DEFAULT_BASE_URL
|
|
67
|
+
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS
|
|
68
|
+
max_retries: int = DEFAULT_RETRIES
|
|
69
|
+
retry_backoff_seconds: float = DEFAULT_RETRY_BACKOFF_SECONDS
|
|
70
|
+
client_identifier: str = DEFAULT_CLIENT_IDENTIFIER
|
|
71
|
+
default_headers: Mapping[str, str] = field(default_factory = lambda: dict(DEFAULT_HEADERS))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class HttpClient:
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
proxy: str | None = None,
|
|
79
|
+
config: HttpClientConfig | None = None,
|
|
80
|
+
session: tls_client.Session | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
self._config: HttpClientConfig = config or HttpClientConfig()
|
|
83
|
+
self._proxy: Proxy = Proxy(proxy)
|
|
84
|
+
self._session: tls_client.Session = session or self._create_session(
|
|
85
|
+
self._proxy,
|
|
86
|
+
client_identifier = self._config.client_identifier,
|
|
87
|
+
)
|
|
88
|
+
self._owns_session: bool = session is None
|
|
89
|
+
|
|
90
|
+
self._session.cookies.set('ogumybbuser', '1')
|
|
91
|
+
self._session.cookies.set('oguloginattempts', '1')
|
|
92
|
+
|
|
93
|
+
async def __aenter__(self) -> HttpClient:
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
97
|
+
self.close()
|
|
98
|
+
|
|
99
|
+
def close(self) -> None:
|
|
100
|
+
if self._owns_session:
|
|
101
|
+
close = getattr(self._session, 'close', None)
|
|
102
|
+
if callable(close):
|
|
103
|
+
close()
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def session(self) -> tls_client.Session:
|
|
107
|
+
return self._session
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def cookies(self) -> Any:
|
|
111
|
+
return self._session.cookies
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def base_url(self) -> str:
|
|
115
|
+
return self._config.base_url
|
|
116
|
+
|
|
117
|
+
async def get(
|
|
118
|
+
self,
|
|
119
|
+
path: str,
|
|
120
|
+
*,
|
|
121
|
+
query: Mapping[str, Any] | None = None,
|
|
122
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
123
|
+
allow_redirects: bool = True,
|
|
124
|
+
) -> tls_client.response.Response:
|
|
125
|
+
return await self.request(
|
|
126
|
+
'GET',
|
|
127
|
+
path,
|
|
128
|
+
query = query,
|
|
129
|
+
extra_headers = extra_headers,
|
|
130
|
+
allow_redirects = allow_redirects,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def post(
|
|
134
|
+
self,
|
|
135
|
+
path: str,
|
|
136
|
+
*,
|
|
137
|
+
data: Mapping[str, Any] | None = None,
|
|
138
|
+
json_body: Any = None,
|
|
139
|
+
query: Mapping[str, Any] | None = None,
|
|
140
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
141
|
+
allow_redirects: bool = True,
|
|
142
|
+
) -> tls_client.response.Response:
|
|
143
|
+
return await self.request(
|
|
144
|
+
'POST',
|
|
145
|
+
path,
|
|
146
|
+
data = data,
|
|
147
|
+
json_body = json_body,
|
|
148
|
+
query = query,
|
|
149
|
+
extra_headers = extra_headers,
|
|
150
|
+
allow_redirects = allow_redirects,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
async def request(
|
|
154
|
+
self,
|
|
155
|
+
method: str,
|
|
156
|
+
path: str,
|
|
157
|
+
*,
|
|
158
|
+
query: Mapping[str, Any] | None = None,
|
|
159
|
+
data: Mapping[str, Any] | None = None,
|
|
160
|
+
json_body: Any = None,
|
|
161
|
+
extra_headers: Mapping[str, str] | None = None,
|
|
162
|
+
allow_redirects: bool = True,
|
|
163
|
+
) -> tls_client.response.Response:
|
|
164
|
+
url = self._build_url(path)
|
|
165
|
+
headers = self._build_headers(extra_headers, has_form_body = data is not None)
|
|
166
|
+
|
|
167
|
+
attempts = max(1, self._config.max_retries + 1)
|
|
168
|
+
last_error: Exception | None = None
|
|
169
|
+
for attempt in range(1, attempts + 1):
|
|
170
|
+
try:
|
|
171
|
+
response = await asyncio.to_thread(
|
|
172
|
+
self._dispatch,
|
|
173
|
+
method = method,
|
|
174
|
+
url = url,
|
|
175
|
+
query = query,
|
|
176
|
+
data = data,
|
|
177
|
+
json_body = json_body,
|
|
178
|
+
headers = headers,
|
|
179
|
+
allow_redirects = allow_redirects,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
except OguTimeoutError as E:
|
|
183
|
+
last_error = E
|
|
184
|
+
if attempt >= attempts:
|
|
185
|
+
raise
|
|
186
|
+
|
|
187
|
+
self._sleep_for_retry(attempt)
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
except OguNetworkError as E:
|
|
191
|
+
last_error = E
|
|
192
|
+
if attempt >= attempts:
|
|
193
|
+
raise
|
|
194
|
+
|
|
195
|
+
self._sleep_for_retry(attempt)
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
if response.status_code in RETRYABLE_STATUS_CODES and attempt < attempts:
|
|
199
|
+
self._sleep_for_retry(attempt)
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
return self._handle_response(response, method = method, url = url)
|
|
203
|
+
|
|
204
|
+
if last_error is not None:
|
|
205
|
+
raise last_error
|
|
206
|
+
|
|
207
|
+
raise OguNetworkError(f'Exhausted retries for {method} {url}')
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _create_session(proxy: Proxy, *, client_identifier: str) -> tls_client.Session:
|
|
211
|
+
session = tls_client.Session(
|
|
212
|
+
client_identifier = client_identifier,
|
|
213
|
+
random_tls_extension_order = True,
|
|
214
|
+
)
|
|
215
|
+
if proxy.proxy:
|
|
216
|
+
session.proxies.update({
|
|
217
|
+
'http': proxy.proxy,
|
|
218
|
+
'https': proxy.proxy,
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
return session
|
|
222
|
+
|
|
223
|
+
def _dispatch(
|
|
224
|
+
self,
|
|
225
|
+
*,
|
|
226
|
+
method: str,
|
|
227
|
+
url: str,
|
|
228
|
+
query: Mapping[str, Any] | None,
|
|
229
|
+
data: Mapping[str, Any] | None,
|
|
230
|
+
json_body: Any,
|
|
231
|
+
headers: Mapping[str, str],
|
|
232
|
+
allow_redirects: bool,
|
|
233
|
+
) -> tls_client.response.Response:
|
|
234
|
+
request_func = getattr(self._session, method.lower())
|
|
235
|
+
try:
|
|
236
|
+
return request_func(
|
|
237
|
+
url,
|
|
238
|
+
params = dict(query) if query else None,
|
|
239
|
+
data = dict(data) if data else None,
|
|
240
|
+
json = json_body,
|
|
241
|
+
headers = dict(headers),
|
|
242
|
+
allow_redirects = allow_redirects,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
except Exception as E:
|
|
246
|
+
message = str(E) or E.__class__.__name__
|
|
247
|
+
if 'timeout' in message.lower() or 'timed out' in message.lower():
|
|
248
|
+
raise OguTimeoutError(f'Request timed out: {method} {url}') from E
|
|
249
|
+
|
|
250
|
+
raise OguNetworkError(f'Network error during {method} {url}: {message}') from E
|
|
251
|
+
|
|
252
|
+
def _build_url(self, path: str) -> str:
|
|
253
|
+
if path.startswith(('http://', 'https://')):
|
|
254
|
+
return path
|
|
255
|
+
|
|
256
|
+
base = self._config.base_url.rstrip('/')
|
|
257
|
+
if not path.startswith('/'):
|
|
258
|
+
path = '/' + path
|
|
259
|
+
|
|
260
|
+
return f'{base}{path}'
|
|
261
|
+
|
|
262
|
+
def _build_headers(
|
|
263
|
+
self,
|
|
264
|
+
extra_headers: Mapping[str, str] | None,
|
|
265
|
+
*,
|
|
266
|
+
has_form_body: bool,
|
|
267
|
+
) -> MutableMapping[str, str]:
|
|
268
|
+
headers: MutableMapping[str, str] = dict(self._config.default_headers)
|
|
269
|
+
if has_form_body:
|
|
270
|
+
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
271
|
+
|
|
272
|
+
if extra_headers:
|
|
273
|
+
for key, value in extra_headers.items():
|
|
274
|
+
headers[key] = value
|
|
275
|
+
|
|
276
|
+
return headers
|
|
277
|
+
|
|
278
|
+
def _sleep_for_retry(self, attempt: int) -> None:
|
|
279
|
+
delay = self._config.retry_backoff_seconds * (2 ** (attempt - 1))
|
|
280
|
+
logger.debug('Retrying in %.2fs (attempt %d)', delay, attempt)
|
|
281
|
+
|
|
282
|
+
def _handle_response(
|
|
283
|
+
self,
|
|
284
|
+
response: tls_client.response.Response,
|
|
285
|
+
*,
|
|
286
|
+
method: str,
|
|
287
|
+
url: str,
|
|
288
|
+
) -> tls_client.response.Response:
|
|
289
|
+
status = response.status_code
|
|
290
|
+
if status < 400:
|
|
291
|
+
return response
|
|
292
|
+
|
|
293
|
+
body = response.text if hasattr(response, 'text') else None
|
|
294
|
+
message = f'{method} {url} -> {status}'
|
|
295
|
+
|
|
296
|
+
if status == 401:
|
|
297
|
+
raise OguAuthenticationError(message, status_code = status, method = method, url = url, body = body)
|
|
298
|
+
|
|
299
|
+
if status == 403:
|
|
300
|
+
raise OguAuthorizationError(message, status_code = status, method = method, url = url, body = body)
|
|
301
|
+
|
|
302
|
+
if status == 404:
|
|
303
|
+
raise OguNotFoundError(message, status_code = status, method = method, url = url, body = body)
|
|
304
|
+
|
|
305
|
+
if status in (400, 422):
|
|
306
|
+
raise OguValidationError(message, status_code = status, method = method, url = url, body = body)
|
|
307
|
+
|
|
308
|
+
if status == 429:
|
|
309
|
+
retry_after = _parse_retry_after(response)
|
|
310
|
+
raise OguRateLimitError(
|
|
311
|
+
message,
|
|
312
|
+
status_code = status,
|
|
313
|
+
method = method,
|
|
314
|
+
url = url,
|
|
315
|
+
body = body,
|
|
316
|
+
retry_after_seconds = retry_after,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if 500 <= status < 600:
|
|
320
|
+
raise OguServerError(message, status_code = status, method = method, url = url, body = body)
|
|
321
|
+
|
|
322
|
+
raise OguAPIError(message, status_code = status, method = method, url = url, body = body)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _parse_retry_after(response: tls_client.response.Response) -> float | None:
|
|
326
|
+
raw = None
|
|
327
|
+
headers = getattr(response, 'headers', None)
|
|
328
|
+
if headers is not None:
|
|
329
|
+
raw = headers.get('Retry-After') or headers.get('retry-after')
|
|
330
|
+
|
|
331
|
+
if raw is None:
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
return float(raw)
|
|
336
|
+
|
|
337
|
+
except (TypeError, ValueError):
|
|
338
|
+
return None
|
ogu_api/client.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import tls_client
|
|
6
|
+
|
|
7
|
+
from ._http import HttpClient, HttpClientConfig
|
|
8
|
+
from .resources import (
|
|
9
|
+
CreditsResource,
|
|
10
|
+
FeedResource,
|
|
11
|
+
MembersResource,
|
|
12
|
+
MessagesResource,
|
|
13
|
+
NotificationsResource,
|
|
14
|
+
ReputationResource,
|
|
15
|
+
SearchResource,
|
|
16
|
+
SessionResource,
|
|
17
|
+
ThreadsResource,
|
|
18
|
+
UserCPResource,
|
|
19
|
+
UsersResource,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = ['OguClient']
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OguClient:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
proxy: str | None = None,
|
|
30
|
+
base_url: str | None = None,
|
|
31
|
+
timeout_seconds: float | None = None,
|
|
32
|
+
max_retries: int | None = None,
|
|
33
|
+
retry_backoff_seconds: float | None = None,
|
|
34
|
+
client_identifier: str | None = None,
|
|
35
|
+
config: HttpClientConfig | None = None,
|
|
36
|
+
session: tls_client.Session | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
resolved_config = self._resolve_config(
|
|
39
|
+
config = config,
|
|
40
|
+
base_url = base_url,
|
|
41
|
+
timeout_seconds = timeout_seconds,
|
|
42
|
+
max_retries = max_retries,
|
|
43
|
+
retry_backoff_seconds = retry_backoff_seconds,
|
|
44
|
+
client_identifier = client_identifier,
|
|
45
|
+
)
|
|
46
|
+
self._http: HttpClient = HttpClient(
|
|
47
|
+
proxy = proxy,
|
|
48
|
+
config = resolved_config,
|
|
49
|
+
session = session,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.session: SessionResource = SessionResource(self._http)
|
|
53
|
+
self.users: UsersResource = UsersResource(self._http)
|
|
54
|
+
self.usercp: UserCPResource = UserCPResource(self._http)
|
|
55
|
+
self.reputation: ReputationResource = ReputationResource(self._http)
|
|
56
|
+
self.credits: CreditsResource = CreditsResource(self._http)
|
|
57
|
+
self.messages: MessagesResource = MessagesResource(self._http)
|
|
58
|
+
self.notifications: NotificationsResource = NotificationsResource(self._http)
|
|
59
|
+
self.feed: FeedResource = FeedResource(self._http)
|
|
60
|
+
self.threads: ThreadsResource = ThreadsResource(self._http)
|
|
61
|
+
self.search: SearchResource = SearchResource(self._http)
|
|
62
|
+
self.members: MembersResource = MembersResource(self._http)
|
|
63
|
+
|
|
64
|
+
async def __aenter__(self) -> OguClient:
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
68
|
+
self.close()
|
|
69
|
+
|
|
70
|
+
def close(self) -> None:
|
|
71
|
+
self._http.close()
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def http(self) -> HttpClient:
|
|
75
|
+
return self._http
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def cookies(self) -> Any:
|
|
79
|
+
return self._http.cookies
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _resolve_config(
|
|
83
|
+
*,
|
|
84
|
+
config: HttpClientConfig | None,
|
|
85
|
+
base_url: str | None,
|
|
86
|
+
timeout_seconds: float | None,
|
|
87
|
+
max_retries: int | None,
|
|
88
|
+
retry_backoff_seconds: float | None,
|
|
89
|
+
client_identifier: str | None,
|
|
90
|
+
) -> HttpClientConfig:
|
|
91
|
+
if config is None:
|
|
92
|
+
base = HttpClientConfig()
|
|
93
|
+
|
|
94
|
+
else:
|
|
95
|
+
base = config
|
|
96
|
+
|
|
97
|
+
return HttpClientConfig(
|
|
98
|
+
base_url = base_url if base_url is not None else base.base_url,
|
|
99
|
+
timeout_seconds = (
|
|
100
|
+
timeout_seconds if timeout_seconds is not None else base.timeout_seconds
|
|
101
|
+
),
|
|
102
|
+
max_retries = max_retries if max_retries is not None else base.max_retries,
|
|
103
|
+
retry_backoff_seconds = (
|
|
104
|
+
retry_backoff_seconds
|
|
105
|
+
if retry_backoff_seconds is not None
|
|
106
|
+
else base.retry_backoff_seconds
|
|
107
|
+
),
|
|
108
|
+
client_identifier = (
|
|
109
|
+
client_identifier if client_identifier is not None else base.client_identifier
|
|
110
|
+
),
|
|
111
|
+
default_headers = base.default_headers,
|
|
112
|
+
)
|
ogu_api/errors.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'OguError',
|
|
7
|
+
'OguAPIError',
|
|
8
|
+
'OguAuthenticationError',
|
|
9
|
+
'OguAuthorizationError',
|
|
10
|
+
'OguNotFoundError',
|
|
11
|
+
'OguValidationError',
|
|
12
|
+
'OguRateLimitError',
|
|
13
|
+
'OguServerError',
|
|
14
|
+
'OguNetworkError',
|
|
15
|
+
'OguTimeoutError',
|
|
16
|
+
'OguParseError',
|
|
17
|
+
'OguSessionError',
|
|
18
|
+
'OguLoginError',
|
|
19
|
+
'OguReputationError',
|
|
20
|
+
'OguCreditsError',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OguError(Exception):
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OguNetworkError(OguError):
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OguTimeoutError(OguNetworkError):
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OguAPIError(OguError):
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
message: str,
|
|
40
|
+
*,
|
|
41
|
+
status_code: int,
|
|
42
|
+
method: str,
|
|
43
|
+
url: str,
|
|
44
|
+
body: str | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
|
|
48
|
+
self.status_code: int = status_code
|
|
49
|
+
self.method: str = method
|
|
50
|
+
self.url: str = url
|
|
51
|
+
self.body: str | None = body
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
base = super().__str__()
|
|
55
|
+
return f'{base} [{self.method} {self.url} -> {self.status_code}]'
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class OguAuthenticationError(OguAPIError):
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class OguAuthorizationError(OguAPIError):
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class OguNotFoundError(OguAPIError):
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OguValidationError(OguAPIError):
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class OguRateLimitError(OguAPIError):
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
message: str,
|
|
78
|
+
*,
|
|
79
|
+
status_code: int,
|
|
80
|
+
method: str,
|
|
81
|
+
url: str,
|
|
82
|
+
body: str | None = None,
|
|
83
|
+
retry_after_seconds: float | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
super().__init__(
|
|
86
|
+
message,
|
|
87
|
+
status_code = status_code,
|
|
88
|
+
method = method,
|
|
89
|
+
url = url,
|
|
90
|
+
body = body,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self.retry_after_seconds: float | None = retry_after_seconds
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class OguServerError(OguAPIError):
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class OguParseError(OguError):
|
|
101
|
+
def __init__(self, message: str = 'Failed to parse response') -> None:
|
|
102
|
+
super().__init__(message)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class OguSessionError(OguError):
|
|
106
|
+
def __init__(self, message: str = 'Session is invalid or expired') -> None:
|
|
107
|
+
super().__init__(message)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class OguLoginError(OguError):
|
|
111
|
+
def __init__(self, message: str = 'Login failed') -> None:
|
|
112
|
+
super().__init__(message)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class OguReputationError(OguError):
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
message: str = 'Reputation request failed',
|
|
119
|
+
*,
|
|
120
|
+
valid_amounts: list[str] | None = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
super().__init__(message)
|
|
123
|
+
|
|
124
|
+
self.valid_amounts: list[str] | None = valid_amounts
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class OguCreditsError(OguError):
|
|
128
|
+
def __init__(self, message: str = 'Credits request failed') -> None:
|
|
129
|
+
super().__init__(message)
|
ogu_api/models.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'UserProfile',
|
|
8
|
+
'ReputationPage',
|
|
9
|
+
'ActionResult',
|
|
10
|
+
'Message',
|
|
11
|
+
'RecentTransaction',
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen = True)
|
|
16
|
+
class UserProfile:
|
|
17
|
+
user_id: str
|
|
18
|
+
username: str | None = None
|
|
19
|
+
reputation: str | None = None
|
|
20
|
+
vouches: str | None = None
|
|
21
|
+
credits: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen = True)
|
|
25
|
+
class ReputationPage:
|
|
26
|
+
values: list[str] = field(default_factory = list)
|
|
27
|
+
hidden: dict[str, Any] = field(default_factory = dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen = True)
|
|
31
|
+
class ActionResult:
|
|
32
|
+
success: bool
|
|
33
|
+
message: str = ''
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen = True)
|
|
37
|
+
class Message:
|
|
38
|
+
username: str
|
|
39
|
+
date: int
|
|
40
|
+
message: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen = True)
|
|
44
|
+
class RecentTransaction:
|
|
45
|
+
sender: str
|
|
46
|
+
recipient: str
|
|
47
|
+
amount: int
|
|
48
|
+
date: int
|