itd-sdk 2.5.1__tar.gz → 2.5.2__tar.gz
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.
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/PKG-INFO +1 -1
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/__init__.py +16 -1
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/_default.py +2 -1
- itd_sdk-2.5.2/itd/_limiter.py +97 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/base.py +17 -16
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/client.py +14 -11
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/comment.py +2 -2
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/exceptions.py +7 -1
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/notification.py +2 -2
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/request.py +6 -6
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/pyproject.toml +1 -1
- itd_sdk-2.5.1/itd/_limiter.py +0 -107
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/README.md +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/__init__.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/auth.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/comments.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/dwell.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/etc.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/files.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/hashtags.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/notifications.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/pins.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/platform.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/polls.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/posts.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/reports.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/search.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/sessions.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/subscription.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/users.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/verification.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/clan.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/enums.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/file.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/hashtag.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/logger.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/pin.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/poll.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/post.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/report.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/session.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/span.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/user.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/utils.py +0 -0
- {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/version.py +0 -0
|
@@ -32,9 +32,11 @@ iiii iii ttt dd dd
|
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
34
|
from importlib.metadata import version
|
|
35
|
+
from time import sleep
|
|
35
36
|
|
|
36
37
|
__version__ = version("itd-sdk")
|
|
37
38
|
|
|
39
|
+
from itd._default import limiters
|
|
38
40
|
from itd.clan import Clan, TopClans
|
|
39
41
|
from itd.client import Client as ITDClient
|
|
40
42
|
from itd.client import Config as ITDConfig
|
|
@@ -47,7 +49,19 @@ from itd.session import Sessions
|
|
|
47
49
|
from itd.user import Me, User, Users, get_follow_status
|
|
48
50
|
from itd.version import Apps, Changelog
|
|
49
51
|
|
|
52
|
+
|
|
53
|
+
def acquire_limiters():
|
|
54
|
+
try:
|
|
55
|
+
sleep(max(limiter.get_delay() for limiter in limiters.values() if limiter.is_ran))
|
|
56
|
+
except ValueError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
for limiter in limiters.values():
|
|
60
|
+
limiter.is_ran = False
|
|
61
|
+
|
|
62
|
+
|
|
50
63
|
__all__ = [
|
|
64
|
+
'__version__',
|
|
51
65
|
'Changelog',
|
|
52
66
|
'Apps',
|
|
53
67
|
'ITDClient',
|
|
@@ -68,5 +82,6 @@ __all__ = [
|
|
|
68
82
|
'User',
|
|
69
83
|
'Me',
|
|
70
84
|
'Users',
|
|
71
|
-
'get_follow_status'
|
|
85
|
+
'get_follow_status',
|
|
86
|
+
'acquire_limiters'
|
|
72
87
|
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from time import monotonic, sleep
|
|
2
|
+
|
|
3
|
+
from itd.logger import get_logger
|
|
4
|
+
|
|
5
|
+
l = get_logger('limiter')
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RateLimiter:
|
|
9
|
+
window: int = 65
|
|
10
|
+
|
|
11
|
+
def __init__(self, capacity: int):
|
|
12
|
+
l.debug(r'\[%s] create rate limiter', capacity)
|
|
13
|
+
self.capacity = capacity
|
|
14
|
+
self.is_ran = True
|
|
15
|
+
|
|
16
|
+
def sync(self, remaining: int): ...
|
|
17
|
+
def acquire(self): ...
|
|
18
|
+
def on_limit(self): ...
|
|
19
|
+
def get_delay(self): ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SafeRateLimiter(RateLimiter):
|
|
23
|
+
def __init__(self, capacity: int):
|
|
24
|
+
super().__init__(capacity)
|
|
25
|
+
self.delay = 60 / capacity
|
|
26
|
+
self.last_request = monotonic()
|
|
27
|
+
self.requests = 0
|
|
28
|
+
self.is_ran = False
|
|
29
|
+
|
|
30
|
+
def get_delay(self):
|
|
31
|
+
self.requests += 1
|
|
32
|
+
self.last_request = monotonic()
|
|
33
|
+
return max(0.1, self.delay - (monotonic() - self.last_request))
|
|
34
|
+
|
|
35
|
+
def acquire(self):
|
|
36
|
+
l.debug(r'\[%s] acquire limiter delay=%s', self.capacity, round(self.delay, 2))
|
|
37
|
+
sleep(self.get_delay())
|
|
38
|
+
|
|
39
|
+
def sync(self, remaining: int):
|
|
40
|
+
l.info(r'\[%s] sync limiter remaining=%s', self.capacity, remaining)
|
|
41
|
+
if remaining < self.capacity * 0.2:
|
|
42
|
+
self.delay *= 1 + 1 / (remaining or 0.5)
|
|
43
|
+
l.info(r'\[%s] increase delay=%s', self.capacity, round(self.delay, 2))
|
|
44
|
+
if remaining > self.capacity * 0.9:
|
|
45
|
+
self.delay *= 0.7
|
|
46
|
+
l.info(r'\[%s] decrease delay=%s', self.capacity, round(self.delay, 2))
|
|
47
|
+
elif 10 < self.requests < 500 and remaining > self.capacity * 0.5:
|
|
48
|
+
self.delay -= 1 / self.requests
|
|
49
|
+
self.delay = max(0.1, min(self.delay, 30))
|
|
50
|
+
self.is_ran = True
|
|
51
|
+
|
|
52
|
+
def on_limit(self):
|
|
53
|
+
self.delay *= 2
|
|
54
|
+
self.delay = min(self.delay, 30)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BurstRateLimiter(RateLimiter):
|
|
58
|
+
def __init__(self, capacity: int):
|
|
59
|
+
super().__init__(capacity)
|
|
60
|
+
self.requests = []
|
|
61
|
+
|
|
62
|
+
def sync(self, remaining: int):
|
|
63
|
+
l.debug(r'\[%s] sync limiter remaining=%s', self.capacity, remaining)
|
|
64
|
+
|
|
65
|
+
used = self.capacity - remaining
|
|
66
|
+
if used > 0:
|
|
67
|
+
self.requests = self.requests[-used:] # ai
|
|
68
|
+
while len(self.requests) < used:
|
|
69
|
+
self.requests.append(monotonic())
|
|
70
|
+
|
|
71
|
+
def acquire(self):
|
|
72
|
+
l.debug(r'\[%s] acquire limiter', self.capacity)
|
|
73
|
+
|
|
74
|
+
self.requests = [request for request in self.requests if request > monotonic() - self.window]
|
|
75
|
+
while len(self.requests) >= self.capacity:
|
|
76
|
+
self.requests = [request for request in self.requests if request > monotonic() - self.window]
|
|
77
|
+
if self.requests:
|
|
78
|
+
sleep(max(self.requests[0] + self.window - monotonic(), 0.1))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class IPRateLimiter:
|
|
82
|
+
window: int = 60
|
|
83
|
+
max_requests: int = 90
|
|
84
|
+
|
|
85
|
+
def __init__(self):
|
|
86
|
+
self.requests = []
|
|
87
|
+
|
|
88
|
+
def acquire(self):
|
|
89
|
+
self.requests.append(monotonic())
|
|
90
|
+
self.requests = [request for request in self.requests if request > monotonic() - self.window]
|
|
91
|
+
while len(self.requests) >= self.max_requests:
|
|
92
|
+
self.requests = [request for request in self.requests if request > monotonic() - self.window]
|
|
93
|
+
if self.requests:
|
|
94
|
+
sleep(self.get_delay())
|
|
95
|
+
|
|
96
|
+
def get_delay(self):
|
|
97
|
+
return max(self.requests[0] + self.window - monotonic(), 0.1)
|
|
@@ -9,7 +9,7 @@ from pydantic import BaseModel
|
|
|
9
9
|
from requests import Response
|
|
10
10
|
from requests.exceptions import JSONDecodeError
|
|
11
11
|
|
|
12
|
-
from itd._default import get_default_client, ip_limiter, limiters
|
|
12
|
+
from itd._default import get_default_client, ip_limiter, limiters, limits
|
|
13
13
|
from itd._limiter import BurstRateLimiter, SafeRateLimiter
|
|
14
14
|
from itd.enums import ALL, BATCH, All, Batch, DebugResponseMode, LoadStatus
|
|
15
15
|
from itd.exceptions import DEFAULT_ERRORS, AccessTokenExpiredError, AccountDeletedError, ITDException, RateLimitError, UnauthorizedError, ValidationError
|
|
@@ -100,10 +100,10 @@ class ITDBaseModel:
|
|
|
100
100
|
|
|
101
101
|
try:
|
|
102
102
|
value = object.__getattribute__(self, name)
|
|
103
|
-
|
|
104
|
-
except AttributeError:
|
|
103
|
+
exc = None
|
|
104
|
+
except AttributeError as e:
|
|
105
105
|
value = None
|
|
106
|
-
|
|
106
|
+
exc = e
|
|
107
107
|
|
|
108
108
|
if (
|
|
109
109
|
_getattr(self, '_refreshable')
|
|
@@ -119,8 +119,8 @@ class ITDBaseModel:
|
|
|
119
119
|
self.refresh()
|
|
120
120
|
return object.__getattribute__(self, name)
|
|
121
121
|
|
|
122
|
-
if not
|
|
123
|
-
raise
|
|
122
|
+
if exc is not None:
|
|
123
|
+
raise exc
|
|
124
124
|
return value
|
|
125
125
|
|
|
126
126
|
|
|
@@ -317,13 +317,12 @@ def api_wrapper(*exceptions: ITDException):
|
|
|
317
317
|
def decorator(func):
|
|
318
318
|
@wraps(func)
|
|
319
319
|
def wrapper(client: Client, *args, **kwargs) -> Response | None:
|
|
320
|
+
name = func.__name__
|
|
321
|
+
|
|
320
322
|
def exec():
|
|
321
|
-
name = func.__name__
|
|
322
|
-
if name in ('get_followers', 'get_following'):
|
|
323
|
-
name = 'get_follow'
|
|
324
323
|
l.info('exec %s %s %s', func.__name__, _filter_bytes(args), kwargs)
|
|
325
|
-
if name in
|
|
326
|
-
limiters[name].acquire()
|
|
324
|
+
if client.config.auto_acquire and name in limits:
|
|
325
|
+
limiters[limits[name]].acquire()
|
|
327
326
|
ip_limiter.acquire()
|
|
328
327
|
|
|
329
328
|
res: Response = func(client, *args, **kwargs)
|
|
@@ -336,14 +335,15 @@ def api_wrapper(*exceptions: ITDException):
|
|
|
336
335
|
|
|
337
336
|
remaining = int(res.headers.get('x-ratelimit-remaining', 0))
|
|
338
337
|
limit = int(res.headers.get('x-ratelimit-limit', 0))
|
|
338
|
+
limits[name] = limit
|
|
339
339
|
|
|
340
340
|
if client.config._anti_rate_limit:
|
|
341
|
-
if client.config._burst_requests and
|
|
342
|
-
limiters[
|
|
341
|
+
if client.config._burst_requests and limit not in limiters:
|
|
342
|
+
limiters[limit] = BurstRateLimiter(limit)
|
|
343
343
|
|
|
344
|
-
elif
|
|
345
|
-
limiters[
|
|
346
|
-
limiters[
|
|
344
|
+
elif limit not in limiters:
|
|
345
|
+
limiters[limit] = SafeRateLimiter(limit)
|
|
346
|
+
limiters[limit].sync(remaining)
|
|
347
347
|
|
|
348
348
|
if client.config.debug_response == DebugResponseMode.BEFORE:
|
|
349
349
|
l.debug('response (raw): %s', res.text)
|
|
@@ -409,6 +409,7 @@ def api_wrapper(*exceptions: ITDException):
|
|
|
409
409
|
retry_after = getattr(e, 'retry_after') or client.config.retry_delay
|
|
410
410
|
l.warning('%s on %s: wait %ss', e.__class__.__name__, func.__name__, retry_after)
|
|
411
411
|
sleep(retry_after)
|
|
412
|
+
limiters[name].on_limit()
|
|
412
413
|
|
|
413
414
|
return wrapper
|
|
414
415
|
|
|
@@ -17,7 +17,7 @@ from itd.api.auth import change_password, logout, refresh_token
|
|
|
17
17
|
from itd.api.posts import get_stats
|
|
18
18
|
from itd.api.search import search
|
|
19
19
|
from itd.enums import BATCH, All, AuthLevel, Batch, DebugResponseMode, ParseMode, RateLimitMode, Role, UserAgent
|
|
20
|
-
from itd.exceptions import InsufficientAuthLevelError,
|
|
20
|
+
from itd.exceptions import InsufficientAuthLevelError, NotFoundError, RateLimitError
|
|
21
21
|
from itd.hashtag import Hashtag, Hashtags
|
|
22
22
|
from itd.logger import get_logger
|
|
23
23
|
from itd.post import DwellTracker, Post
|
|
@@ -80,6 +80,7 @@ class Config:
|
|
|
80
80
|
burst_requests: bool | None = None
|
|
81
81
|
anti_ip_ban: bool = True
|
|
82
82
|
limit_coefficient: float | None = None
|
|
83
|
+
auto_acquire: bool = True
|
|
83
84
|
|
|
84
85
|
# enable_logging: bool | None = None
|
|
85
86
|
# logging_level = 'DEBUG'
|
|
@@ -101,8 +102,8 @@ class Config:
|
|
|
101
102
|
timeout_file: float | None = None
|
|
102
103
|
timeout_file_download: float | None = None
|
|
103
104
|
|
|
104
|
-
url: str = 'xn--d1ah4a.com'
|
|
105
|
-
url_api: str | None = None
|
|
105
|
+
url: str = 'https://xn--d1ah4a.com/api'
|
|
106
|
+
url_api: str | None = None # deprecated
|
|
106
107
|
user_agent: UserAgent | str = UserAgent.BROWSER
|
|
107
108
|
solve_challenge: bool = True
|
|
108
109
|
|
|
@@ -134,10 +135,12 @@ class Config:
|
|
|
134
135
|
|
|
135
136
|
on_exceptions: dict[type[Exception], Callable[[Exception], None]] = field(default_factory=dict)
|
|
136
137
|
batch_sizes: BatchSizes = field(default_factory=BatchSizes)
|
|
138
|
+
refresh_token_cookie_name: str = 'refresh_token'
|
|
137
139
|
|
|
138
140
|
def __post_init__(self):
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
if self.url_api is not None:
|
|
142
|
+
l.warning('config.url_api is deprecated and will be removed in 2.7.0. Please use config.url.')
|
|
143
|
+
self.url = self.url_api
|
|
141
144
|
|
|
142
145
|
match self.user_agent:
|
|
143
146
|
case UserAgent.DEFAULT:
|
|
@@ -206,14 +209,14 @@ class Config:
|
|
|
206
209
|
|
|
207
210
|
|
|
208
211
|
class AccessToken(BaseModel):
|
|
209
|
-
roles: list[Role]
|
|
212
|
+
roles: list[Role] = [Role.USER]
|
|
210
213
|
session_id: UUID = Field(alias='sid')
|
|
211
|
-
is_active: bool = Field(alias='isActive')
|
|
214
|
+
is_active: bool = Field(True, alias='isActive')
|
|
212
215
|
subject_id: UUID = Field(alias='sub')
|
|
213
216
|
issued_at: datetime = Field(alias='iat')
|
|
214
|
-
issuer: str = Field(alias='iss') # "auth-service"
|
|
217
|
+
issuer: str | None = Field(None, alias='iss') # "auth-service"
|
|
215
218
|
expired_at: datetime = Field(alias='exp')
|
|
216
|
-
jwt_id: UUID = Field(alias='jti')
|
|
219
|
+
jwt_id: UUID | None = Field(None, alias='jti')
|
|
217
220
|
|
|
218
221
|
@field_validator('issued_at', 'expired_at', mode='plain')
|
|
219
222
|
@classmethod
|
|
@@ -242,9 +245,9 @@ class Client:
|
|
|
242
245
|
self.access_token = access.replace('Bearer ', '')
|
|
243
246
|
|
|
244
247
|
if refresh:
|
|
248
|
+
self.session.cookies.set(config.refresh_token_cookie_name, refresh, path='/')
|
|
245
249
|
self.auth_level = AuthLevel.REFRESH
|
|
246
250
|
self.refresh_token = refresh
|
|
247
|
-
self.session.cookies.set('refresh_token', refresh, path='/', domain=self.config.url)
|
|
248
251
|
# if access is None:
|
|
249
252
|
# self.refresh_auth()
|
|
250
253
|
|
|
@@ -352,7 +355,7 @@ class Client:
|
|
|
352
355
|
res = refresh_token(self)
|
|
353
356
|
res.raise_for_status()
|
|
354
357
|
|
|
355
|
-
self.access_token = res.json()
|
|
358
|
+
self.access_token = res.json().get('accessToken') or res.json()['token']
|
|
356
359
|
self.access_token_data = AccessToken.model_validate(decode_jwt_payload(self.access_token))
|
|
357
360
|
|
|
358
361
|
assert self.access_token
|
|
@@ -41,9 +41,9 @@ class Comment(ITDBaseModel):
|
|
|
41
41
|
_base_comment: Comment | None = None
|
|
42
42
|
|
|
43
43
|
@classmethod
|
|
44
|
-
def from_dict(cls, data: dict, post: Post | None = None, base_comment: Comment | None = None, *, client: Client | None = None) -> Comment:
|
|
44
|
+
def from_dict(cls, data: dict, post: Post | None = None, base_comment: Comment | None = None, *, client: Client | None = None) -> Comment:
|
|
45
45
|
instance = super().from_dict(data, client=client)
|
|
46
|
-
if post:
|
|
46
|
+
if post is not None:
|
|
47
47
|
instance._post = post
|
|
48
48
|
instance._base_comment = base_comment
|
|
49
49
|
instance.replies._post = post
|
|
@@ -97,6 +97,11 @@ class InvalidAccessTokenError(AuthError):
|
|
|
97
97
|
json_check = staticmethod(lambda json: json.get('error') == 'invalid signature')
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
class JWTAlgorithmUnsupportedError(AuthError):
|
|
101
|
+
json_check = staticmethod(lambda json: json.get('error') == 'Unsupported token algorithm')
|
|
102
|
+
text = 'Access token JWT algorithm unsupported'
|
|
103
|
+
|
|
104
|
+
|
|
100
105
|
class SessionRevokedError(AuthError):
|
|
101
106
|
code = 'SESSION_REVOKED'
|
|
102
107
|
text = 'Session revoked (logged out)'
|
|
@@ -104,7 +109,7 @@ class SessionRevokedError(AuthError):
|
|
|
104
109
|
|
|
105
110
|
class AccessTokenExpiredError(AuthError):
|
|
106
111
|
text = 'Token expired'
|
|
107
|
-
json_check = staticmethod(lambda json: json.get('error') == 'token expired')
|
|
112
|
+
json_check = staticmethod(lambda json: json.get('error') == 'token expired' or json.get('message') == 'Invalid or expired token')
|
|
108
113
|
|
|
109
114
|
|
|
110
115
|
class LoginError(AuthError):
|
|
@@ -345,6 +350,7 @@ DEFAULT_ERRORS = (
|
|
|
345
350
|
RateLimitError(),
|
|
346
351
|
InvalidAccessTokenError(),
|
|
347
352
|
UnauthorizedError(),
|
|
353
|
+
JWTAlgorithmUnsupportedError(),
|
|
348
354
|
AccessTokenExpiredError(),
|
|
349
355
|
AccountBannedError(),
|
|
350
356
|
AccountTemporarilyBannedError(),
|
|
@@ -168,12 +168,12 @@ class Notifications(ITDList[Notification]):
|
|
|
168
168
|
|
|
169
169
|
l.info('stop stream')
|
|
170
170
|
|
|
171
|
-
def stream_bg(self) -> Thread:
|
|
171
|
+
def stream_bg(self, daemon: bool = False) -> Thread:
|
|
172
172
|
def _stream():
|
|
173
173
|
for _ in self.stream():
|
|
174
174
|
continue
|
|
175
175
|
|
|
176
|
-
thread = Thread(target=_stream)
|
|
176
|
+
thread = Thread(target=_stream, daemon=daemon)
|
|
177
177
|
thread.start()
|
|
178
178
|
return thread
|
|
179
179
|
|
|
@@ -29,7 +29,7 @@ def _get_jhash(b: int) -> int:
|
|
|
29
29
|
return k
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def _solve_ddos_guard(session: Session, response: Response,
|
|
32
|
+
def _solve_ddos_guard(session: Session, response: Response, user_agent: str = '') -> bool:
|
|
33
33
|
"""Solve DDoS-Guard JS challenge. Returns True if solved (duplicate request required)."""
|
|
34
34
|
if '<html>' not in response.text[:500] or 'get_jhash' not in response.text:
|
|
35
35
|
return False
|
|
@@ -45,8 +45,8 @@ def _solve_ddos_guard(session: Session, response: Response, domain: str = 'xn--d
|
|
|
45
45
|
jhash = _get_jhash(code)
|
|
46
46
|
l.info('solved jhash=%s', jhash)
|
|
47
47
|
|
|
48
|
-
session.cookies.set('__jhash_', str(jhash),
|
|
49
|
-
session.cookies.set('__jua_', quote(user_agent, safe=''),
|
|
48
|
+
session.cookies.set('__jhash_', str(jhash), path='/')
|
|
49
|
+
session.cookies.set('__jua_', quote(user_agent, safe=''), path='/')
|
|
50
50
|
|
|
51
51
|
return True
|
|
52
52
|
|
|
@@ -97,11 +97,11 @@ def fetch(client: 'Client', method: str, url: str, params: dict = {}, files: dic
|
|
|
97
97
|
def _do_request():
|
|
98
98
|
m = method.lower()
|
|
99
99
|
if m == "get":
|
|
100
|
-
return client.session.get(f'{client.config.
|
|
100
|
+
return client.session.get(f'{client.config.url}/{url}', timeout=client.config.timeout, params=params, headers=headers)
|
|
101
101
|
|
|
102
102
|
return client.session.request(
|
|
103
103
|
m.upper(),
|
|
104
|
-
f'{client.config.
|
|
104
|
+
f'{client.config.url}/{url}',
|
|
105
105
|
timeout=client.config.timeout_file if files else client.config.timeout,
|
|
106
106
|
json=params,
|
|
107
107
|
headers=headers,
|
|
@@ -111,7 +111,7 @@ def fetch(client: 'Client', method: str, url: str, params: dict = {}, files: dic
|
|
|
111
111
|
res = _do_request()
|
|
112
112
|
if client.config.solve_challenge:
|
|
113
113
|
for _ in range(3):
|
|
114
|
-
if not _solve_ddos_guard(client.session, res, client.config.
|
|
114
|
+
if not _solve_ddos_guard(client.session, res, client.config._user_agent):
|
|
115
115
|
break
|
|
116
116
|
l.debug('ddos-guard cookies: %s', {c.name: c.value for c in client.session.cookies if c.name.startswith('__')})
|
|
117
117
|
res = _do_request()
|
itd_sdk-2.5.1/itd/_limiter.py
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
from time import monotonic, sleep
|
|
2
|
-
|
|
3
|
-
from itd.logger import get_logger
|
|
4
|
-
|
|
5
|
-
l = get_logger('limiter')
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class RateLimiter:
|
|
9
|
-
window: int = 65
|
|
10
|
-
|
|
11
|
-
def __init__(self, name: str, capacity: int):
|
|
12
|
-
l.debug(r'\[%s] create rate limiter limit=%s', name, capacity)
|
|
13
|
-
self.name = name
|
|
14
|
-
self.capacity = capacity
|
|
15
|
-
|
|
16
|
-
def sync(self, remaining: int): ...
|
|
17
|
-
def acquire(self): ...
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class SafeRateLimiter(RateLimiter):
|
|
21
|
-
def __init__(self, name: str, capacity: int):
|
|
22
|
-
super().__init__(name, capacity)
|
|
23
|
-
self.refill_rate = 1.5
|
|
24
|
-
self.requests = 0
|
|
25
|
-
self.remainings = []
|
|
26
|
-
|
|
27
|
-
def acquire(self):
|
|
28
|
-
l.debug(r'\[%s] acquire limiter delay=%s', self.name, round(self.refill_rate, 2))
|
|
29
|
-
sleep(self.refill_rate)
|
|
30
|
-
self.requests += 1
|
|
31
|
-
|
|
32
|
-
def sync(self, remaining: int):
|
|
33
|
-
l.debug(r'\[%s] sync limiter remaining=%s', self.name, remaining)
|
|
34
|
-
if self.requests > 50:
|
|
35
|
-
return
|
|
36
|
-
if self.requests == 50:
|
|
37
|
-
l.info(r'\[%s] stop learning final=%s', self.name, round(self.refill_rate, 2))
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
self.remainings.append(remaining or -5)
|
|
41
|
-
if len(self.remainings) > 5:
|
|
42
|
-
if self.remainings[-5] > remaining + 2:
|
|
43
|
-
value = self.refill_rate * (1 + (self.remainings[-5] - remaining) / 8)
|
|
44
|
-
l.info(
|
|
45
|
-
r'\[%s] increase refill rate %s -> %s (remaining %s -> %s)',
|
|
46
|
-
self.name,
|
|
47
|
-
round(self.refill_rate, 2),
|
|
48
|
-
round(value, 2),
|
|
49
|
-
self.remainings[-5],
|
|
50
|
-
remaining
|
|
51
|
-
)
|
|
52
|
-
self.refill_rate = value
|
|
53
|
-
self.remainings.clear()
|
|
54
|
-
elif self.remainings[-5] < remaining - 2:
|
|
55
|
-
value = self.refill_rate * (1 - (self.remainings[-5] - remaining) / 8)
|
|
56
|
-
l.info(
|
|
57
|
-
r'\[%s] decrease refill rate %s -> %s (remaining %s -> %s)',
|
|
58
|
-
self.name,
|
|
59
|
-
round(self.refill_rate, 2),
|
|
60
|
-
round(value, 2),
|
|
61
|
-
self.remainings[-5],
|
|
62
|
-
remaining
|
|
63
|
-
)
|
|
64
|
-
self.refill_rate = value
|
|
65
|
-
self.remainings.clear()
|
|
66
|
-
else:
|
|
67
|
-
l.debug(r'\[%s] refill rate is stable', self.name)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class BurstRateLimiter(RateLimiter):
|
|
71
|
-
def __init__(self, name: str, capacity: int):
|
|
72
|
-
super().__init__(name, capacity)
|
|
73
|
-
self.requests = []
|
|
74
|
-
|
|
75
|
-
def sync(self, remaining: int):
|
|
76
|
-
l.debug(r'\[%s] sync limiter remaining=%s', self.name, remaining)
|
|
77
|
-
|
|
78
|
-
used = self.capacity - remaining
|
|
79
|
-
if used > 0:
|
|
80
|
-
self.requests = self.requests[-used:] # ai
|
|
81
|
-
while len(self.requests) < used:
|
|
82
|
-
self.requests.append(monotonic())
|
|
83
|
-
|
|
84
|
-
def acquire(self):
|
|
85
|
-
l.debug(r'\[%s] acquire limiter', self.name)
|
|
86
|
-
|
|
87
|
-
self.requests = [request for request in self.requests if request > monotonic() - self.window]
|
|
88
|
-
while len(self.requests) >= self.capacity:
|
|
89
|
-
self.requests = [request for request in self.requests if request > monotonic() - self.window]
|
|
90
|
-
if self.requests:
|
|
91
|
-
sleep(max(self.requests[0] + self.window - monotonic(), 0.1))
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
class IPRateLimiter:
|
|
95
|
-
window: int = 60
|
|
96
|
-
max_requests: int = 100
|
|
97
|
-
|
|
98
|
-
def __init__(self):
|
|
99
|
-
self.requests = []
|
|
100
|
-
|
|
101
|
-
def acquire(self):
|
|
102
|
-
self.requests.append(monotonic())
|
|
103
|
-
self.requests = [request for request in self.requests if request > monotonic() - self.window]
|
|
104
|
-
while len(self.requests) >= self.max_requests:
|
|
105
|
-
self.requests = [request for request in self.requests if request > monotonic() - self.window]
|
|
106
|
-
if self.requests:
|
|
107
|
-
sleep(max(self.requests[0] + self.window - monotonic(), 0.1))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|