itd-sdk 2.5.2__tar.gz → 2.6.1__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 (46) hide show
  1. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/PKG-INFO +2 -2
  2. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/README.md +1 -1
  3. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/__init__.py +12 -6
  4. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/_default.py +22 -2
  5. itd_sdk-2.6.1/itd/_limiter.py +96 -0
  6. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/comments.py +14 -9
  7. itd_sdk-2.6.1/itd/api/notifications.py +62 -0
  8. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/posts.py +0 -5
  9. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/base.py +72 -23
  10. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/client.py +91 -42
  11. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/comment.py +88 -41
  12. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/enums.py +12 -2
  13. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/hashtag.py +4 -5
  14. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/notification.py +144 -18
  15. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/poll.py +7 -20
  16. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/post.py +85 -78
  17. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/request.py +2 -2
  18. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/user.py +54 -50
  19. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/utils.py +1 -1
  20. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/pyproject.toml +1 -1
  21. itd_sdk-2.5.2/itd/_limiter.py +0 -97
  22. itd_sdk-2.5.2/itd/api/notifications.py +0 -39
  23. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/__init__.py +0 -0
  24. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/auth.py +0 -0
  25. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/dwell.py +0 -0
  26. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/etc.py +0 -0
  27. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/files.py +0 -0
  28. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/hashtags.py +0 -0
  29. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/pins.py +0 -0
  30. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/platform.py +0 -0
  31. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/polls.py +0 -0
  32. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/reports.py +0 -0
  33. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/search.py +0 -0
  34. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/sessions.py +0 -0
  35. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/subscription.py +0 -0
  36. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/users.py +0 -0
  37. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/api/verification.py +0 -0
  38. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/clan.py +0 -0
  39. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/exceptions.py +0 -0
  40. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/file.py +0 -0
  41. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/logger.py +0 -0
  42. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/pin.py +0 -0
  43. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/report.py +0 -0
  44. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/session.py +0 -0
  45. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/span.py +0 -0
  46. {itd_sdk-2.5.2 → itd_sdk-2.6.1}/itd/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itd-sdk
3
- Version: 2.5.2
3
+ Version: 2.6.1
4
4
  Summary: ITD client for python
5
5
  Keywords: itd,itdsocial
6
6
  Author: fi-res
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
29
29
 
30
30
  # itd-sdk
31
31
 
32
- [![ncruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/fi-res/ncruff)
32
+ [![ncruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/fi-res/ncruff) [![EffVer Versioning](https://img.shields.io/badge/version_scheme-EffVer-0097a7)](https://jacobtomlinson.dev/effver)
33
33
 
34
34
  SDK для работы с [https://xn--d1ah4a.com](итд.com) на python.
35
35
 
@@ -1,6 +1,6 @@
1
1
  # itd-sdk
2
2
 
3
- [![ncruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/fi-res/ncruff)
3
+ [![ncruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/fi-res/ncruff) [![EffVer Versioning](https://img.shields.io/badge/version_scheme-EffVer-0097a7)](https://jacobtomlinson.dev/effver)
4
4
 
5
5
  SDK для работы с [https://xn--d1ah4a.com](итд.com) на python.
6
6
 
@@ -36,7 +36,9 @@ from time import sleep
36
36
 
37
37
  __version__ = version("itd-sdk")
38
38
 
39
- from itd._default import limiters
39
+ from itd._default import LimiterConfig, limiters
40
+ from itd._default import set_config as set_limiter_config
41
+ from itd._limiter import BurstRateLimiter, HalfRateLimiter, IPRateLimiter, RateLimiter
40
42
  from itd.clan import Clan, TopClans
41
43
  from itd.client import Client as ITDClient
42
44
  from itd.client import Config as ITDConfig
@@ -50,14 +52,12 @@ from itd.user import Me, User, Users, get_follow_status
50
52
  from itd.version import Apps, Changelog
51
53
 
52
54
 
55
+ # call if you set auto_acquire=False in the end of cycle
53
56
  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
57
+ sleep(max((limiter.delay for limiter in limiters.values() if limiter.used), default=0))
58
58
 
59
59
  for limiter in limiters.values():
60
- limiter.is_ran = False
60
+ limiter.used = False
61
61
 
62
62
 
63
63
  __all__ = [
@@ -83,5 +83,11 @@ __all__ = [
83
83
  'Me',
84
84
  'Users',
85
85
  'get_follow_status',
86
+ 'set_limiter_config',
87
+ 'LimiterConfig',
88
+ 'BurstRateLimiter',
89
+ 'RateLimiter',
90
+ 'HalfRateLimiter',
91
+ 'IPRateLimiter',
86
92
  'acquire_limiters'
87
93
  ]
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  from itd._limiter import IPRateLimiter, RateLimiter
@@ -10,13 +11,32 @@ if TYPE_CHECKING:
10
11
  _default_client: Client | None = None
11
12
  limiters: dict[int, RateLimiter] = {}
12
13
  limits: dict[str, int] = {}
13
- ip_limiter = IPRateLimiter()
14
+
15
+
16
+ @dataclass
17
+ class LimiterConfig:
18
+ limiter: type[RateLimiter] | None = None
19
+ ip_limiter: IPRateLimiter | None = None
20
+ auto_acquire: bool = True
21
+
22
+
23
+ _config = LimiterConfig()
24
+
25
+
26
+ def set_config(config: LimiterConfig):
27
+ global _config
28
+ _config = config
29
+
30
+
31
+ def get_config() -> LimiterConfig:
32
+ global _config
33
+ return _config
14
34
 
15
35
 
16
36
  def get_default_client() -> Client:
17
37
  global _default_client
18
38
  if _default_client is None:
19
- raise
39
+ raise RuntimeError('No client inited')
20
40
  return _default_client
21
41
 
22
42
 
@@ -0,0 +1,96 @@
1
+ from abc import ABC, abstractmethod
2
+ from time import monotonic, sleep
3
+
4
+ from itd.logger import get_logger
5
+
6
+ l = get_logger('limiter')
7
+
8
+
9
+ class RateLimiter(ABC):
10
+ def __init__(self, capacity: int):
11
+ l.debug(r'\[%s] create rate limiter', capacity)
12
+ self.capacity = capacity
13
+ self._delay = 0
14
+ self.used = False
15
+
16
+ @abstractmethod
17
+ def sync(self, remaining: int): ... # sync remaining, calls after request
18
+
19
+ def acquire(self): # wait, calls before request
20
+ sleep(self.delay)
21
+
22
+ @abstractmethod
23
+ def on_limit(self): ... # calls when RateLimitError raised
24
+
25
+ def request(self): # call before request
26
+ self.used = True
27
+
28
+ @property
29
+ def delay(self):
30
+ return self._delay
31
+
32
+
33
+ class HalfRateLimiter(RateLimiter):
34
+ def __init__(self, capacity: int):
35
+ super().__init__(capacity)
36
+ self._delay = 60 / capacity
37
+ self.last_request = 0
38
+ self.requests = 0
39
+
40
+ @property
41
+ def delay(self):
42
+ return max(0.1, self._delay - (monotonic() - self.last_request))
43
+
44
+ def request(self):
45
+ super().request()
46
+ self.last_request = monotonic()
47
+ self.requests += 1
48
+
49
+ def sync(self, remaining: int):
50
+ # короче тут такая задумка что лимитер оставляет половину ременинга, то есть чем ты дальше от центра тем сильнее тебя пиздит лимитер
51
+ best = self.capacity / 2
52
+
53
+ def diff(a, b):
54
+ return max(a, b) - min(a, b)
55
+
56
+ l.info(r'\[%s] sync limiter total=%s remaining=%s diff=%s', self.capacity, self.requests, remaining, diff(best, remaining))
57
+ if remaining < best - 3 and self.requests > best:
58
+ self._delay = min(self._delay * (1 + (diff(best, remaining) / 20)), 30)
59
+ l.info(r'\[%s] slowdown delay=%.2f', self.capacity, self._delay)
60
+
61
+ elif remaining > best + 3 and self.requests > best:
62
+ self._delay = max(self._delay * (1 - (diff(best, remaining) / 50)), 0.1)
63
+ l.info(r'\[%s] speedup delay=%.2f', self.capacity, self._delay)
64
+
65
+ def on_limit(self):
66
+ self._delay *= 2
67
+ self._delay = min(self._delay, 30)
68
+
69
+
70
+ class BurstRateLimiter(RateLimiter):
71
+ def __init__(self, capacity: int):
72
+ super().__init__(capacity)
73
+ self.remaining = capacity
74
+
75
+ def sync(self, remaining: int):
76
+ l.debug(r'\[%s] sync limiter remaining=%s', self.capacity, remaining)
77
+ self.remaining = remaining
78
+
79
+ @property
80
+ def delay(self):
81
+ if self.remaining < 3:
82
+ return 60
83
+ return 0
84
+
85
+ def on_limit(self): ...
86
+
87
+
88
+ class IPRateLimiter:
89
+ def __init__(self):
90
+ self.requests = []
91
+
92
+ def acquire(self):
93
+ self.requests.append(monotonic())
94
+ self.requests = [request for request in self.requests if request > monotonic() - 60]
95
+ if len(self.requests) >= 90:
96
+ sleep(max(self.requests[0] + 60 - monotonic(), 0))
@@ -41,20 +41,25 @@ def get_comments(client: Client, post_id: UUID, cursor: int = 0, limit: int = 20
41
41
 
42
42
 
43
43
  @api_wrapper(NotFoundError('Comment'))
44
- def like_comment(client: Client, comment_id: UUID):
45
- return client.request('post', f'comments/{comment_id}/like')
44
+ def like_comment(client: Client, id: UUID):
45
+ return client.request('post', f'comments/{id}/like')
46
46
 
47
47
 
48
48
  @api_wrapper(NotFoundError('Comment'))
49
- def unlike_comment(client: Client, comment_id: UUID):
50
- return client.request('delete', f'comments/{comment_id}/like')
49
+ def unlike_comment(client: Client, id: UUID):
50
+ return client.request('delete', f'comments/{id}/like')
51
51
 
52
52
 
53
- @api_wrapper(NotFoundError('Comment'), AlreadyDeletedError('Comment'))
54
- def delete_comment(client: Client, comment_id: UUID):
55
- return client.request('delete', f'comments/{comment_id}')
53
+ @api_wrapper(NotFoundError('Comment'), AlreadyDeletedError('Comment'), ForbiddenError('delete comment'))
54
+ def delete_comment(client: Client, id: UUID):
55
+ return client.request('delete', f'comments/{id}')
56
56
 
57
57
 
58
58
  @api_wrapper(ValidationError(), NotFoundError('Comment'))
59
- def get_replies(client: Client, comment_id: UUID, page: int = 1, limit: int = 50):
60
- return client.request('get', f'comments/{comment_id}/replies', {'page': page, 'limit': limit})
59
+ def get_replies(client: Client, id: UUID, page: int = 1, limit: int = 50):
60
+ return client.request('get', f'comments/{id}/replies', {'page': page, 'limit': limit})
61
+
62
+
63
+ @api_wrapper(ValidationError(), NotFoundError('Comment'), ForbiddenError('edit comment'))
64
+ def edit_comment(client: Client, id: UUID, content: str):
65
+ return client.request('patch', f'comments/{id}', {'content': content})
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+ from uuid import UUID
5
+
6
+ from itd.base import api_wrapper
7
+ from itd.exceptions import NotFoundError
8
+
9
+ if TYPE_CHECKING:
10
+ from itd.client import Client
11
+ from itd.notification import NotificationsSettings
12
+
13
+
14
+ @api_wrapper()
15
+ def get_notifications(client: Client, limit: int = 20, offset: int = 0):
16
+ return client.request('get', 'notifications', {'limit': limit, 'offset': offset})
17
+
18
+
19
+ @api_wrapper(NotFoundError('Notification', json_check=lambda json: json.get('success') is False))
20
+ def mark_as_read(client: Client, id: UUID):
21
+ return client.request('post', f'notifications/{id}/read')
22
+
23
+
24
+ @api_wrapper()
25
+ def mark_all_as_read(client: Client):
26
+ return client.request('post', 'notifications/read-all')
27
+
28
+
29
+ @api_wrapper()
30
+ def get_unread_notifications_count(client: Client):
31
+ return client.request('get', 'notifications/count')
32
+
33
+
34
+ def stream_notifications(client: Client):
35
+ return client.request_sse('notifications/stream')
36
+
37
+
38
+ def get_notifications_settings(client: Client):
39
+ return client.request('get', 'notifications/settings')
40
+
41
+
42
+ def update_notifications_settings(client: Client, settings: NotificationsSettings, *, old: bool = True, new: bool = True):
43
+ from itd.notification import _NotificationsSettingsNew, _NotificationsSettingsNewPreferences, _NotificationsSettingsOld # жду фикс circular import день 67
44
+
45
+ data = {}
46
+ if old:
47
+ print(settings.follows)
48
+
49
+ data.update(_NotificationsSettingsOld.model_validate(settings, from_attributes=True).model_dump(mode='json', by_alias=True))
50
+ print(settings.follows)
51
+
52
+ if new:
53
+ data.update(
54
+ _NotificationsSettingsNew(
55
+ web_enabled=settings.web_enabled,
56
+ sound_enabled=settings.sound,
57
+ preferences=_NotificationsSettingsNewPreferences.model_validate(settings, from_attributes=True),
58
+ enabled=settings.enabled
59
+ ).model_dump(mode='json', by_alias=True)
60
+ )
61
+
62
+ return client.request('put', 'notifications/settings', data)
@@ -97,11 +97,6 @@ def repost(client: Client, id: UUID, content: str | None = None):
97
97
  return client.request('post', f'posts/{id}/repost', data)
98
98
 
99
99
 
100
- @api_wrapper(NotFoundError('Post'))
101
- def view_post(client: Client, id: UUID):
102
- return client.request('post', f'posts/{id}/view')
103
-
104
-
105
100
  @api_wrapper(ValidationError(), NotFoundError('User'))
106
101
  def get_liked_posts(client: Client, username_or_id: str | UUID, cursor: datetime | None = None, limit: int = 20):
107
102
  return client.request('get', f'posts/user/{username_or_id}/liked', {'limit': limit, 'cursor': cursor})
@@ -4,13 +4,14 @@ from abc import abstractmethod
4
4
  from functools import wraps
5
5
  from time import sleep
6
6
  from typing import TYPE_CHECKING, Any, Callable, Iterator, SupportsIndex, TypeVar, cast, overload
7
+ from uuid import UUID
7
8
 
8
9
  from pydantic import BaseModel
10
+ from pydantic.fields import FieldInfo
9
11
  from requests import Response
10
12
  from requests.exceptions import JSONDecodeError
11
13
 
12
- from itd._default import get_default_client, ip_limiter, limiters, limits
13
- from itd._limiter import BurstRateLimiter, SafeRateLimiter
14
+ from itd._default import get_config, get_default_client, limiters, limits
14
15
  from itd.enums import ALL, BATCH, All, Batch, DebugResponseMode, LoadStatus
15
16
  from itd.exceptions import DEFAULT_ERRORS, AccessTokenExpiredError, AccountDeletedError, ITDException, RateLimitError, UnauthorizedError, ValidationError
16
17
  from itd.logger import get_logger
@@ -50,7 +51,9 @@ class ITDBaseModel:
50
51
 
51
52
  def __init__(self, client: Client | None = None) -> None:
52
53
  self._client = client or get_default_client()
54
+
53
55
  self._loaded_attrs: set[str] = set()
56
+ self._extra_context = {}
54
57
 
55
58
  def __setattr__(self, name: str, value: Any) -> None:
56
59
  if isinstance(value, ITDBaseModel) and (client := _getattr(self, '_client')): # ai
@@ -72,32 +75,33 @@ class ITDBaseModel:
72
75
  if not self._refreshable:
73
76
  l.warning(f'{self.__class__.__name__} is not refreshable but refresh is called')
74
77
  self.load_status = LoadStatus.LOADING
75
- self._fill_from_data(self._refresh(client=client or self.client))
78
+ self._fill_from_data(self._refresh(client=client or self.client), context={'client': client or self.client})
76
79
  self.load_status = LoadStatus.FULL
77
80
  return self
78
81
 
79
- def _fill_from_data(self, data: dict):
82
+ def _fill_from_data(self, data: dict, *, context: dict = {}):
80
83
  assert self._validator, 'Unable to use fill_from_data without a validator'
81
- validated = self._validator().model_validate(data) # ty: ignore[missing-argument]
84
+ context.update(self._extra_context)
85
+ validated = self._validator().model_validate(data, context=context) # ty: ignore[missing-argument]
82
86
  self._loaded_attrs = validated.model_fields_set # значения автоматом добавляются через setattr # так значит это же тогда надо закоментить? # хз наверн
83
87
  for name, value in validated.__dict__.items():
84
- setattr(self, name, value)
88
+ object.__setattr__(self, name, value)
85
89
 
86
90
  self._post_refresh()
87
91
 
88
92
  @classmethod
89
- def from_dict(cls, data: dict, *, client: Client | None = None):
93
+ def from_dict(cls, data: dict, *, context: dict = {}, client: Client | None = None):
90
94
  instance = cls.__new__(cls)
91
95
  ITDBaseModel.__init__(instance, client)
92
96
 
93
- instance._fill_from_data(data)
97
+ context.setdefault('client', client or instance.client)
98
+ instance._fill_from_data(data, context=context)
94
99
  instance.load_status = LoadStatus.PARTIALLY
95
100
  return instance
96
101
 
97
102
  if not TYPE_CHECKING:
98
103
 
99
104
  def __getattribute__(self, name: str) -> Any:
100
-
101
105
  try:
102
106
  value = object.__getattribute__(self, name)
103
107
  exc = None
@@ -111,14 +115,38 @@ class ITDBaseModel:
111
115
  and name not in ('client', 'model_fields_set', 'load_status')
112
116
  and not callable(value)
113
117
  and not isinstance(_getattr(type(self), name), property)
114
- and name not in _getattr(self, '_loaded_attrs')
115
118
  and _getattr(self, 'load_status') in (LoadStatus.NO, LoadStatus.PARTIALLY)
119
+ and name not in (_getattr(self, '_loaded_attrs') or {name})
120
+ and self.client.config.load_on_getattr
116
121
  and not (isinstance(value, ITDBaseModel) and not value._load_with_parent)
117
122
  ):
118
123
  l.info('refresh %s field=%s load_status=%s', self.__class__.__name__, name, _getattr(self, 'load_status').value)
119
124
  self.refresh()
120
125
  return object.__getattribute__(self, name)
121
126
 
127
+ annotations = {}
128
+ [annotations.update(_getattr(c, '__annotations__')) for c in _getattr(self, '__class__').__mro__ if _getattr(c, '__annotations__')]
129
+
130
+ if isinstance(value, FieldInfo) or (not name.startswith('_') and exc is not None and not _getattr(self, 'client').config.load_on_getattr):
131
+ value_type = annotations[name]
132
+ if 'None' in value_type:
133
+ l.warning('returned fake value None for %s: please load data (call refresh()) before get attributes', name)
134
+ return None
135
+ elif value_type == 'int':
136
+ l.warning('returned fake value 0 for %s: please load data (call refresh()) before get attributes', name)
137
+ return 0
138
+ elif value_type == 'UUID':
139
+ l.warning('returned fake value 00000000-0000-0000-0000-000000000010 for %s: please load data (call refresh()) before get attributes', name)
140
+ return UUID('00000000-0000-0000-0000-000000000000')
141
+ elif value_type == 'str':
142
+ l.warning('returned fake value "" for %s: please load data (call refresh()) before get attributes', name)
143
+ return ''
144
+ elif value_type == 'bool':
145
+ l.warning('returned fake value False for %s: please load data (call refresh()) before get attributes', name)
146
+ return False # дилема
147
+ l.error('cannot fake data for %s (type %s): please load data (call refresh()) before get attributes', name, value_type)
148
+ raise RuntimeError(f'cannot fake data for {name} (type {value_type}): please load data (call refresh()) before get attributes')
149
+
122
150
  if exc is not None:
123
151
  raise exc
124
152
  return value
@@ -134,6 +162,7 @@ class ITDList(ITDBaseModel, list[T]):
134
162
  _refreshable = False
135
163
  has_more = True
136
164
  idx = 0
165
+ _is_page_pagination: bool = False
137
166
 
138
167
  def _fetch(self, client: Client, limit: int) -> dict:
139
168
  return {}
@@ -155,9 +184,15 @@ class ITDList(ITDBaseModel, list[T]):
155
184
  l.warning('skip load because has_more=False')
156
185
  return []
157
186
 
158
- limit = limit or (client or self.client).config.batch_sizes._values[self.__class__.__name__.lower()]
187
+ default_limit = (client or self.client).config.batch_sizes._values[self.__class__.__name__.lower()]
188
+ limit = limit or default_limit
159
189
  if isinstance(count, int) and count < limit:
160
190
  limit = count
191
+
192
+ if self._is_page_pagination and limit != default_limit:
193
+ l.warning('unable to change limit in %s (page pagination), using default %s', self.__class__.__name__.lower(), default_limit)
194
+ count = limit = default_limit
195
+
161
196
  l.debug('load %s count=%s limit=%s cursor=%s', self.__class__.__name__.lower(), count, limit, self.cursor)
162
197
 
163
198
  # Batch = load one batch (limit), All = load everything, int = load exactly N
@@ -263,7 +298,7 @@ class ITDList(ITDBaseModel, list[T]):
263
298
  if value is None or isinstance(self.client.config.load_on_getitem, All):
264
299
  l.debug('getitem load all')
265
300
  self.load_all()
266
- elif isinstance(self.client.config.load_on_getitem, Batch):
301
+ elif self._is_page_pagination or isinstance(self.client.config.load_on_getitem, Batch):
267
302
  l.debug('getitem load batch')
268
303
  self.load(BATCH)
269
304
  else:
@@ -277,7 +312,7 @@ class ITDList(ITDBaseModel, list[T]):
277
312
  if getattr(self, 'total', None) and self.idx >= self.total:
278
313
  raise StopIteration()
279
314
  if self.idx >= len(self) and (self.has_more or self.client.config.force_load_lists):
280
- l.debug('not enough items to call next - load')
315
+ l.debug('not enough items to call next, load')
281
316
  self.load(self.client.config.load_on_iter)
282
317
  if self.idx >= len(self):
283
318
  raise StopIteration()
@@ -295,6 +330,11 @@ class ITDList(ITDBaseModel, list[T]):
295
330
  def all(self) -> list[T]:
296
331
  return self.load_all()
297
332
 
333
+ def clear(self):
334
+ super().clear()
335
+ self.cursor = None
336
+ self.has_more = True
337
+
298
338
 
299
339
  def _filter_bytes(args: tuple):
300
340
  filtered = []
@@ -306,7 +346,7 @@ def _filter_bytes(args: tuple):
306
346
  return filtered
307
347
 
308
348
 
309
- # user calls `Me` -> model calls `get_me` -> `api_wrapper` wrapper: (`get_me` -> `client.request` -> `fetch` -> responses 401 -> `refresh_auth` from `api_wrapper` -> `client.resuest` -> `fetch` -> token refreshed -> `api_wrapper` backs to main query -> `get_me` -> `client.request` -> `fetch` -> user fetched) -> model recieves data -> pydantic fills model # hell what the monster i did
349
+ # user calls `Me` -> model calls `get_me` -> `api_wrapper` wrapper: (`get_me` -> `client.request` -> `fetch` -> responses 401 -> `refresh_auth` from `api_wrapper` -> `client.resuest` -> `fetch` -> token refreshed -> `api_wrapper` backs to main query -> `get_me` -> `client.request` -> `fetch` -> user fetched) -> model recieves data -> pydantic fills model
310
350
  def api_wrapper(*exceptions: ITDException):
311
351
  """Декоратор для отлавливания ошибок
312
352
 
@@ -321,9 +361,19 @@ def api_wrapper(*exceptions: ITDException):
321
361
 
322
362
  def exec():
323
363
  l.info('exec %s %s %s', func.__name__, _filter_bytes(args), kwargs)
324
- if client.config.auto_acquire and name in limits:
325
- limiters[limits[name]].acquire()
326
- ip_limiter.acquire()
364
+
365
+ config = get_config()
366
+ is_first = True
367
+ if name in limits and limits[name] in limiters:
368
+ limiter = limiters[limits[name]]
369
+ if config.auto_acquire:
370
+ limiter.acquire()
371
+ is_first = not limiter.used
372
+ limiter.request()
373
+
374
+ ip_limiter = config.ip_limiter
375
+ if ip_limiter:
376
+ ip_limiter.acquire()
327
377
 
328
378
  res: Response = func(client, *args, **kwargs)
329
379
 
@@ -337,12 +387,10 @@ def api_wrapper(*exceptions: ITDException):
337
387
  limit = int(res.headers.get('x-ratelimit-limit', 0))
338
388
  limits[name] = limit
339
389
 
340
- if client.config._anti_rate_limit:
341
- if client.config._burst_requests and limit not in limiters:
342
- limiters[limit] = BurstRateLimiter(limit)
390
+ if limit not in limiters and config.limiter is not None:
391
+ limiters[limit] = config.limiter(limit)
343
392
 
344
- elif limit not in limiters:
345
- limiters[limit] = SafeRateLimiter(limit)
393
+ if limit in limiters and (config.auto_acquire or is_first):
346
394
  limiters[limit].sync(remaining)
347
395
 
348
396
  if client.config.debug_response == DebugResponseMode.BEFORE:
@@ -409,7 +457,8 @@ def api_wrapper(*exceptions: ITDException):
409
457
  retry_after = getattr(e, 'retry_after') or client.config.retry_delay
410
458
  l.warning('%s on %s: wait %ss', e.__class__.__name__, func.__name__, retry_after)
411
459
  sleep(retry_after)
412
- limiters[name].on_limit()
460
+ if name in limits and limits[name] in limiters:
461
+ limiters[limits[name]].on_limit()
413
462
 
414
463
  return wrapper
415
464