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.
Files changed (45) hide show
  1. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/PKG-INFO +1 -1
  2. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/__init__.py +16 -1
  3. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/_default.py +2 -1
  4. itd_sdk-2.5.2/itd/_limiter.py +97 -0
  5. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/base.py +17 -16
  6. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/client.py +14 -11
  7. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/comment.py +2 -2
  8. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/exceptions.py +7 -1
  9. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/notification.py +2 -2
  10. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/request.py +6 -6
  11. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/pyproject.toml +1 -1
  12. itd_sdk-2.5.1/itd/_limiter.py +0 -107
  13. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/README.md +0 -0
  14. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/__init__.py +0 -0
  15. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/auth.py +0 -0
  16. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/comments.py +0 -0
  17. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/dwell.py +0 -0
  18. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/etc.py +0 -0
  19. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/files.py +0 -0
  20. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/hashtags.py +0 -0
  21. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/notifications.py +0 -0
  22. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/pins.py +0 -0
  23. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/platform.py +0 -0
  24. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/polls.py +0 -0
  25. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/posts.py +0 -0
  26. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/reports.py +0 -0
  27. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/search.py +0 -0
  28. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/sessions.py +0 -0
  29. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/subscription.py +0 -0
  30. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/users.py +0 -0
  31. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/api/verification.py +0 -0
  32. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/clan.py +0 -0
  33. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/enums.py +0 -0
  34. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/file.py +0 -0
  35. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/hashtag.py +0 -0
  36. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/logger.py +0 -0
  37. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/pin.py +0 -0
  38. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/poll.py +0 -0
  39. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/post.py +0 -0
  40. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/report.py +0 -0
  41. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/session.py +0 -0
  42. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/span.py +0 -0
  43. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/user.py +0 -0
  44. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/utils.py +0 -0
  45. {itd_sdk-2.5.1 → itd_sdk-2.5.2}/itd/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itd-sdk
3
- Version: 2.5.1
3
+ Version: 2.5.2
4
4
  Summary: ITD client for python
5
5
  Keywords: itd,itdsocial
6
6
  Author: fi-res
@@ -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
  ]
@@ -8,7 +8,8 @@ if TYPE_CHECKING:
8
8
  from itd.client import Client
9
9
 
10
10
  _default_client: Client | None = None
11
- limiters: dict[str, RateLimiter] = {}
11
+ limiters: dict[int, RateLimiter] = {}
12
+ limits: dict[str, int] = {}
12
13
  ip_limiter = IPRateLimiter()
13
14
 
14
15
 
@@ -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
- exists = True
104
- except AttributeError:
103
+ exc = None
104
+ except AttributeError as e:
105
105
  value = None
106
- exists = False
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 exists:
123
- raise AttributeError
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 limiters:
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 name not in limiters:
342
- limiters[name] = BurstRateLimiter(name, limit)
341
+ if client.config._burst_requests and limit not in limiters:
342
+ limiters[limit] = BurstRateLimiter(limit)
343
343
 
344
- elif name not in limiters:
345
- limiters[name] = SafeRateLimiter(name, limit)
346
- limiters[name].sync(remaining)
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, InternalError, NotFoundError, RateLimitError
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
- self._url_api = self.url_api if self.url_api else f'https://{self.url}/api'
140
- self.url = self.url.split('https://')[0].split('http://')[0]
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()['accessToken']
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: # ty: ignore[invalid-method-override]
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, domain: str = 'xn--d1ah4a.com', user_agent: str = '') -> bool:
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), domain=domain, path='/')
49
- session.cookies.set('__jua_', quote(user_agent, safe=''), domain=domain, path='/')
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._url_api}/{url}', timeout=client.config.timeout, params=params, headers=headers)
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._url_api}/{url}',
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.url, client.config._user_agent):
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()
@@ -14,7 +14,7 @@ version_provider = "uv"
14
14
 
15
15
  [project]
16
16
  name = "itd-sdk"
17
- version = "2.5.1"
17
+ version = "2.5.2"
18
18
  description = "ITD client for python"
19
19
  readme = "README.md"
20
20
  authors = [
@@ -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