itd-sdk 2.8.0__tar.gz → 2.8.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 (57) hide show
  1. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/PKG-INFO +2 -1
  2. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/__init__.py +12 -4
  3. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/hashtags.py +5 -7
  4. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/platform.py +5 -1
  5. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/users.py +7 -4
  6. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/base.py +22 -10
  7. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/client.py +24 -24
  8. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/config.py +1 -3
  9. itd_sdk-2.8.2/itd/core/session_file.py +27 -0
  10. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/utils.py +7 -5
  11. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/enums.py +11 -8
  12. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/exceptions.py +4 -14
  13. itd_sdk-2.8.2/itd/models/announcement.py +74 -0
  14. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/clan.py +1 -8
  15. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/comment.py +22 -8
  16. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/hashtag.py +3 -2
  17. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/notification.py +6 -5
  18. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/post.py +9 -3
  19. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/report.py +4 -3
  20. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/session.py +6 -4
  21. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/user.py +14 -13
  22. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/pyproject.toml +2 -1
  23. itd_sdk-2.8.0/itd/api/etc.py +0 -16
  24. itd_sdk-2.8.0/itd/core/credfile.py +0 -43
  25. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/README.md +0 -0
  26. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/__init__.py +0 -0
  27. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/auth.py +0 -0
  28. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/comments.py +0 -0
  29. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/dwell.py +0 -0
  30. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/files.py +0 -0
  31. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/notifications.py +0 -0
  32. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/pins.py +0 -0
  33. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/polls.py +0 -0
  34. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/portal.py +0 -0
  35. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/posts.py +0 -0
  36. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/reports.py +0 -0
  37. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/search.py +0 -0
  38. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/sessions.py +0 -0
  39. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/subscription.py +0 -0
  40. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/api/verification.py +0 -0
  41. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/__init__.py +0 -0
  42. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/default.py +0 -0
  43. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/dwell.py +0 -0
  44. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/limiter.py +0 -0
  45. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/logger.py +0 -0
  46. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/request.py +0 -0
  47. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/timer.py +0 -0
  48. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/core/visibility.py +0 -0
  49. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/__init__.py +0 -0
  50. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/file.py +0 -0
  51. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/pin.py +0 -0
  52. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/poll.py +0 -0
  53. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/portal.py +0 -0
  54. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/span.py +0 -0
  55. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/utils.py +0 -0
  56. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/models/version.py +0 -0
  57. {itd_sdk-2.8.0 → itd_sdk-2.8.2}/itd/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itd-sdk
3
- Version: 2.8.0
3
+ Version: 2.8.2
4
4
  Summary: ITD client for python
5
5
  Keywords: itd,itdsocial
6
6
  Author: fi-res
@@ -17,6 +17,7 @@ Requires-Dist: sseclient-py~=1.9.0
17
17
  Requires-Dist: telegramify-markdown>=0.5.4
18
18
  Requires-Dist: lxml>=6.1.1
19
19
  Requires-Dist: platformdirs>=4.10.0
20
+ Requires-Dist: websockets>=16.1.1
20
21
  Requires-Dist: dateparser>=1.2.2 ; extra == 'date'
21
22
  Requires-Dist: filetype>=1.2.0 ; extra == 'filetype'
22
23
  Requires-Dist: rich>=15.0.0 ; extra == 'logs'
@@ -36,16 +36,18 @@ from time import sleep
36
36
 
37
37
  __version__ = version('itd-sdk')
38
38
 
39
+ from itd.core.client import Client as ITDClient
40
+ from itd.core.client import init_client
41
+ from itd.core.config import Config as ITDConfig
39
42
  from itd.core.default import LimiterConfig, limiters
40
43
  from itd.core.default import set_config as set_limiter_config
41
44
  from itd.core.limiter import BurstRateLimiter, HalfRateLimiter, IPRateLimiter, RateLimiter
45
+ from itd.models.announcement import Announcement, Announcements
42
46
  from itd.models.clan import Clan, TopClans
43
- from itd.core.client import Client as ITDClient
44
- from itd.core.config import Config as ITDConfig
45
- from itd.core.client import init_client
47
+ from itd.models.comment import Comment
46
48
  from itd.models.file import File
47
49
  from itd.models.hashtag import Hashtag, Hashtags
48
- from itd.models.notification import Notifications
50
+ from itd.models.notification import Notification, Notifications, Ntf, Ntfs
49
51
  from itd.models.poll import NewPoll
50
52
  from itd.models.portal import Portal
51
53
  from itd.models.post import HashtagPosts, LikedPosts, Post, Posts, UserPosts
@@ -69,12 +71,18 @@ __all__ = [
69
71
  'ITDClient',
70
72
  'ITDConfig',
71
73
  'init_client',
74
+ 'Comment',
72
75
  'Clan',
73
76
  'TopClans',
74
77
  'File',
78
+ 'Announcement',
79
+ 'Announcements',
75
80
  'Hashtag',
76
81
  'Hashtags',
77
82
  'Notifications',
83
+ 'Notification',
84
+ 'Ntfs',
85
+ 'Ntf',
78
86
  'Post',
79
87
  'Posts',
80
88
  'UserPosts',
@@ -3,19 +3,14 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
  from uuid import UUID
5
5
 
6
+ from itd.core.request import endpoint
6
7
  from itd.enums import AuthLevel
7
8
  from itd.exceptions import NotFoundError, TooLargeError, ValidationError
8
- from itd.core.request import endpoint
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from itd.core.client import Client
12
12
 
13
13
 
14
- @endpoint('get', 'hashtags', ValidationError(), level=AuthLevel.NO)
15
- def search_hashtags(client: Client, query: str, limit: int = 10):
16
- return {'q': query, 'limit': limit}
17
-
18
-
19
14
  @endpoint('get', 'hashtags/trending', ValidationError(), level=AuthLevel.NO)
20
15
  def get_hashtags(client: Client, limit: int = 10):
21
16
  return {'limit': limit}
@@ -30,4 +25,7 @@ def get_hashtags(client: Client, limit: int = 10):
30
25
  level=AuthLevel.NO
31
26
  )
32
27
  def get_posts_by_hashtag(client: Client, hashtag: str, cursor: UUID | None = None, limit: int = 20):
33
- return {'limit': limit, 'cursor': cursor}
28
+ query: dict = {'limit': limit}
29
+ if cursor:
30
+ query['cursor'] = cursor
31
+ return query
@@ -2,8 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from itd.enums import AuthLevel
6
5
  from itd.core.request import endpoint
6
+ from itd.enums import AuthLevel
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from itd.core.client import Client
@@ -15,3 +15,7 @@ def get_apps(client: Client): ...
15
15
 
16
16
  @endpoint('get', 'platform/changelog', level=AuthLevel.NO)
17
17
  def get_changelog(client: Client): ...
18
+
19
+
20
+ @endpoint('get', 'platform/announcements', level=AuthLevel.NO)
21
+ def get_announcements(client: Client): ...
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
  from uuid import UUID
5
5
 
6
+ from itd.core.request import endpoint
6
7
  from itd.enums import UNSET, AccessType, AuthLevel, Unset
7
8
  from itd.exceptions import (
8
9
  AlreadyBlockedError,
@@ -20,7 +21,6 @@ from itd.exceptions import (
20
21
  UsernameTakenError,
21
22
  ValidationError
22
23
  )
23
- from itd.core.request import endpoint
24
24
 
25
25
  if TYPE_CHECKING:
26
26
  from itd.core.client import Client
@@ -133,6 +133,9 @@ def get_follow_status(client: Client, user_ids: list[UUID]):
133
133
  return {'userIds': list(map(str, user_ids))}
134
134
 
135
135
 
136
- @endpoint('get', 'users/search')
137
- def search_users(client: Client, query: str, limit: int = 10):
138
- return {'q': query, 'limit': limit}
136
+ @endpoint('get', 'users/stats/top-clans')
137
+ def get_top_clans(client: Client): ...
138
+
139
+
140
+ @endpoint('get', 'users/suggestions/who-to-follow')
141
+ def get_who_to_follow(client: Client): ...
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from abc import abstractmethod
4
4
  from functools import cache
5
- from typing import TYPE_CHECKING, Any, Iterator, SupportsIndex, TypeVar, cast, overload
5
+ from typing import TYPE_CHECKING, Any, Iterator, Literal, SupportsIndex, TypeVar, cast, overload
6
6
  from uuid import UUID
7
7
 
8
8
  from pydantic import BaseModel
@@ -21,7 +21,7 @@ l = get_logger('base') # noqa: E741 # seriously, whats wrong that i am using "l
21
21
 
22
22
 
23
23
  @cache
24
- def validator_for(cls: type) -> type[BaseModel]:
24
+ def get_pydantic_model(cls: type) -> type[BaseModel]:
25
25
  """Класс-валидатор модели: собирается один раз на модель при первой валидации"""
26
26
  # __module__ for correct forward refs in annotations
27
27
  return type(f'_{cls.__name__}Validate', (BaseModel, cls), {'__module__': cls.__module__})
@@ -96,12 +96,12 @@ class ITDBaseModel:
96
96
  return self
97
97
 
98
98
  def _fill_from_data(self, data: dict, *, context: dict | None = None):
99
- assert self._validator, 'Unable to use fill_from_data without a validator'
100
- context = dict(context or {}) # копия: дефолт {} общий на все вызовы, его нельзя мутировать
99
+ assert self._pydantic_model, 'Unable to use fill_from_data without a validator'
100
+ context = dict(context or {})
101
101
  context.update(self._extra_context)
102
- validated = self._validator.model_validate(data, context=context) # вложенные модели строятся с клиентом родителя
103
- self._loaded_attrs = validated.model_fields_set # значения автоматом добавляются через setattr # так значит это же тогда надо закоментить? # хз наверн
104
- for name, value in validated.__dict__.items():
102
+ model = self._pydantic_model.model_validate(data, context=context) # вложенные модели строятся с клиентом родителя
103
+ self._loaded_attrs = model.model_fields_set # значения автоматом добавляются через setattr # так значит это же тогда надо закоментить? # хз наверн
104
+ for name, value in model.__dict__.items():
105
105
  object.__setattr__(self, name, value)
106
106
 
107
107
  self._post_refresh(context=context)
@@ -114,9 +114,18 @@ class ITDBaseModel:
114
114
 
115
115
  context.setdefault('client', instance.client)
116
116
  instance._fill_from_data(data, context=context)
117
- instance.load_status = LoadStatus.PARTIALLY
117
+ if instance.load_status == LoadStatus.NO:
118
+ instance.load_status = LoadStatus.PARTIALLY
118
119
  return instance
119
120
 
121
+ def to_dict(self, *, mode: Literal['python', 'json'] = 'python', by_alias: bool = False):
122
+ model = self._pydantic_model.model_validate(self, from_attributes=True, by_alias=False, by_name=True)
123
+ # for name, value in model.__dict__.items():
124
+ # object.__setattr__(model, name, value)
125
+ return {
126
+ k: v.to_dict() if isinstance(v, ITDBaseModel) else v for k, v in model.model_dump(exclude={'load_status'}, mode=mode, by_alias=by_alias).items()
127
+ }
128
+
120
129
  @classmethod
121
130
  def __get_pydantic_core_schema__(cls, source, handler):
122
131
  if issubclass(source, BaseModel):
@@ -126,9 +135,11 @@ class ITDBaseModel:
126
135
  lambda data, info: data if isinstance(data, source) else source.from_dict(data, context=dict(info.context or {}))
127
136
  )
128
137
 
138
+ # def __pydantic_serializer__
139
+
129
140
  @property
130
- def _validator(self) -> type[BaseModel]:
131
- return validator_for(type(self)) # один класс на модель, а не на объект
141
+ def _pydantic_model(self) -> type[BaseModel]:
142
+ return get_pydantic_model(type(self)) # один класс на модель, а не на объект
132
143
 
133
144
  if not TYPE_CHECKING:
134
145
 
@@ -194,6 +205,7 @@ class ITDList(ITDBaseModel, list[T]):
194
205
  has_more = True
195
206
  idx = 0
196
207
  _is_page_pagination: bool = False
208
+ cursor = None
197
209
 
198
210
  def _fetch(self, client: Client, limit: int) -> dict:
199
211
  return {}
@@ -14,12 +14,12 @@ from requests.adapters import HTTPAdapter
14
14
 
15
15
  from itd.api.auth import change_password, logout, refresh_token
16
16
  from itd.core.config import Config
17
- from itd.core.credfile import Credfile
18
17
  from itd.core.default import _default_client, set_default_client
19
18
  from itd.core.dwell import DwellTracker
20
19
  from itd.core.logger import get_logger
21
20
  from itd.core.request import decode_jwt_payload, fetch, fetch_stream
22
- from itd.core.utils import get_credfile, shorten_token
21
+ from itd.core.session_file import SessionFile
22
+ from itd.core.utils import get_session_file, shorten_token
23
23
  from itd.core.visibility import VisibilityTracker
24
24
  from itd.enums import AuthLevel, Role
25
25
  from itd.exceptions import InsufficientAuthLevelError, SessionExpiredError, SessionNotFoundError, SessionRevokedError
@@ -56,7 +56,7 @@ class Client:
56
56
  self.access_token: str | None = None
57
57
  self.access_token_data: AccessToken | None = None
58
58
  self.refresh_token: str | None = None
59
- self._credfile: Credfile | None = None
59
+ self._session_file: SessionFile | None = None
60
60
  self._refresh_lock = RLock() # so background timers and main thread dont refresh token simultaneously # еба он мой стиль коментов спиздил
61
61
 
62
62
  self.session = Session()
@@ -83,31 +83,31 @@ class Client:
83
83
 
84
84
  @classmethod
85
85
  def from_file(cls, name: str, initial_refresh: str | None = None, verify_refresh: bool = False, config: Config = Config()):
86
- credfile = get_credfile(name)
87
- l.debug('get credentials file refresh=%s access=%s', shorten_token(credfile.refresh), shorten_token(credfile.access))
88
- if not credfile.valid:
86
+ session_file = get_session_file(name)
87
+ l.debug('get session file refresh=%s access=%s', shorten_token(session_file.refresh), shorten_token(session_file.access))
88
+ if not session_file.valid:
89
89
  l.warning('last refresh token was expired or not found. Please enter a new one:')
90
90
  update = True
91
91
  else:
92
- update = credfile.refresh is None
92
+ update = session_file.refresh is None
93
93
 
94
94
  if update:
95
95
  if initial_refresh is not None:
96
- credfile.refresh = initial_refresh
96
+ session_file.refresh = initial_refresh
97
97
  elif getenv('ITD_REFRESH_TOKEN') is not None:
98
- credfile.refresh = getenv('ITD_REFRESH_TOKEN')
98
+ session_file.refresh = getenv('ITD_REFRESH_TOKEN')
99
99
  else:
100
100
  try:
101
101
  from rich.prompt import Prompt
102
102
  except ImportError:
103
- credfile.refresh = input('refresh token: ')
103
+ session_file.refresh = input('refresh token: ')
104
104
  else:
105
- credfile.refresh = Prompt.ask('[cyan]refresh token[/]')
105
+ session_file.refresh = Prompt.ask('[cyan]refresh token[/]')
106
106
 
107
- instance = cls(credfile.refresh, credfile.access, config=config)
107
+ instance = cls(session_file.refresh, session_file.access, config=config)
108
108
  if instance.access_token_data and instance.access_token_data.expired_at < datetime.now():
109
109
  instance.access_token = instance.access_token_data = None
110
- instance._credfile = credfile
110
+ instance._session_file = session_file
111
111
  if verify_refresh and initial_refresh is not None:
112
112
  instance.refresh_auth(force=True)
113
113
  elif update:
@@ -115,17 +115,17 @@ class Client:
115
115
  return instance
116
116
 
117
117
  def _update_file(self, valid: bool = True):
118
- if self._credfile is None:
118
+ if self._session_file is None:
119
119
  return
120
120
 
121
- l.debug('update credentials file refresh=%s access=%s', shorten_token(self.refresh_token), shorten_token(self.access_token))
121
+ l.debug('update session file refresh=%s access=%s', shorten_token(self.refresh_token), shorten_token(self.access_token))
122
122
  if not valid:
123
123
  l.warning('mark %s as not valid', shorten_token(self.refresh_token))
124
124
 
125
- self._credfile.access = self.access_token
126
- self._credfile.refresh = self.refresh_token
127
- self._credfile.valid = valid
128
- self._credfile.flush()
125
+ self._session_file.access = self.access_token
126
+ self._session_file.refresh = self.refresh_token
127
+ self._session_file.valid = valid
128
+ self._session_file.flush()
129
129
 
130
130
  @property
131
131
  def is_token_expired(self) -> bool:
@@ -158,16 +158,16 @@ class Client:
158
158
  return self.token
159
159
 
160
160
  l.debug('refresh access_token')
161
- if not force and self._credfile and self._credfile.update():
162
- self.token = self._credfile.access
163
- self.refresh_token = self._credfile.refresh
161
+ if not force and self._session_file and self._session_file.update():
162
+ self.token = self._session_file.access
163
+ self.refresh_token = self._session_file.refresh
164
164
 
165
165
  if self.access_token:
166
166
  if not self.is_token_expired:
167
- l.debug('update access_token from credfile')
167
+ l.debug('update access_token from session_file')
168
168
  return self.access_token
169
169
  else:
170
- l.info('credfile access_token expired')
170
+ l.info('session_file access_token expired')
171
171
  else:
172
172
  l.info('crefile access_token is none')
173
173
 
@@ -4,9 +4,9 @@ from typing import Callable, Literal
4
4
  from requests.exceptions import RequestException
5
5
  from requests.utils import default_user_agent
6
6
 
7
+ from itd.core.utils import get_sdk_user_agent
7
8
  from itd.enums import BATCH, All, Batch, DebugResponseMode, ParseMode, UserAgent
8
9
  from itd.exceptions import RateLimitError
9
- from itd.core.utils import get_sdk_user_agent
10
10
 
11
11
 
12
12
  @dataclass
@@ -55,8 +55,6 @@ class Config:
55
55
  # ^ это если что просто мысли, не обращайте внимания
56
56
  client_type: Literal['client', 'bot', 'onetime'] = 'onetime'
57
57
 
58
- auto_acquire: bool | None = None
59
-
60
58
  # enable_logging: bool | None = None
61
59
  # logging_level = 'DEBUG'
62
60
 
@@ -0,0 +1,27 @@
1
+ from datetime import datetime
2
+ from json import dumps, loads
3
+ from pathlib import Path
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SessionFile(BaseModel):
9
+ access: str | None = None
10
+ refresh: str | None = None
11
+ valid: bool = True
12
+ updated_at: datetime = Field(default_factory=datetime.now)
13
+ seen_announcements: list[str] = []
14
+
15
+ _file: Path
16
+
17
+ def flush(self):
18
+ self.updated_at = datetime.now()
19
+ self._file.write_text(dumps(self.model_dump(mode='json')))
20
+
21
+ def update(self):
22
+ new = self.model_validate(loads(self._file.read_text()))
23
+ if self.updated_at <= new.updated_at:
24
+ for field in SessionFile.model_fields:
25
+ setattr(self, field, getattr(new, field))
26
+ return True
27
+ return False
@@ -5,7 +5,7 @@ from uuid import UUID
5
5
 
6
6
  from platformdirs import user_data_path
7
7
 
8
- from itd.core.credfile import Credfile
8
+ from itd.core.session_file import SessionFile
9
9
 
10
10
 
11
11
  def get_sdk_user_agent():
@@ -14,13 +14,13 @@ def get_sdk_user_agent():
14
14
  return f'itd-sdk/{__version__} (Python/{version})'
15
15
 
16
16
 
17
- def get_credfile(name: str) -> Credfile:
17
+ def get_session_file(name: str) -> SessionFile:
18
18
  file = user_data_path('itd_sdk', False, ensure_exists=True) / f'{name}.json'
19
19
  if not file.exists():
20
- model = Credfile(_file=file)
20
+ model = SessionFile(_file=file)
21
21
  file.write_text(dumps(model.model_dump(mode='json')))
22
22
  else:
23
- model = Credfile.model_validate(loads(file.read_text()))
23
+ model = SessionFile.model_validate(loads(file.read_text()))
24
24
  model._file = file
25
25
  return model
26
26
 
@@ -47,7 +47,9 @@ def to_nullable_uuid(value: str | UUID | None) -> UUID | None:
47
47
  return to_uuid(value)
48
48
 
49
49
 
50
- def parse_datetime(value: str) -> datetime:
50
+ def parse_datetime(value: str | datetime) -> datetime:
51
+ if isinstance(value, datetime):
52
+ return value
51
53
  v = value.replace('Z', '+00:00')
52
54
  try:
53
55
  return datetime.strptime(v + '00', '%Y-%m-%d %H:%M:%S.%f%z')
@@ -2,6 +2,16 @@ from enum import Enum
2
2
  from typing import Literal
3
3
 
4
4
 
5
+ class AnnouncementButtonStyle(Enum):
6
+ PRIMARY = 'primary'
7
+ SECONDARY = 'secondary'
8
+
9
+
10
+ class AnnouncementButtonType(Enum):
11
+ DISMISS = 'dismiss'
12
+ LINK = 'link'
13
+
14
+
5
15
  class InteractionType(Enum):
6
16
  PHOTO_OPEN = 1
7
17
  VIDEO_PROGRESS = 2
@@ -34,7 +44,7 @@ class ViewReason(Enum):
34
44
  HIDDEN = 2 # тоже страница скрыта (listener visibilitychange и доп.проверка что document.hidden)
35
45
  PAGE_HIDE = 3 # и это тоже страница скрыта (listener pagehiden)
36
46
  UNOBSERVE = 4 # что то заумное на фронтедерском, пост удалили из дома
37
- THRESHOLD_MET = INACTIVE = 5 # юзер перестал активить
47
+ INACTIVE = 5 # юзер перестал активить
38
48
 
39
49
 
40
50
  class AuthLevel(Enum):
@@ -59,13 +69,6 @@ class AuthLevel(Enum):
59
69
  _AUTH_LEVEL_HIERARCHY = [AuthLevel.NO, AuthLevel.ACCESS, AuthLevel.REFRESH]
60
70
 
61
71
 
62
- class RateLimitMode(Enum):
63
- NO = 'no'
64
- MIN = 'min' # for one-time actions (eg script just to like post)
65
- MID = 'mid' # for client apps / basic scripts
66
- MAX = 'max' # for advanced scripts / userbots
67
-
68
-
69
72
  class DebugResponseMode(Enum):
70
73
  NO = 'no'
71
74
  BEFORE = 'before' # before error checks, raw
@@ -1,5 +1,5 @@
1
1
  from datetime import datetime
2
- from typing import Callable
2
+ from typing import Callable, cast
3
3
 
4
4
  from requests import Response
5
5
 
@@ -30,7 +30,7 @@ class ITDException(Exception):
30
30
  Returns:
31
31
  bool: Совпадение
32
32
  """
33
- error = json.get('error') if isinstance(json.get('error'), dict) else {}
33
+ error = cast(dict, json.get('error')) if isinstance(json.get('error'), dict) else {}
34
34
  return bool(
35
35
  (self.res_check and self.res_check(res))
36
36
  or (self.text_check and self.text_check(res.text))
@@ -41,16 +41,6 @@ class ITDException(Exception):
41
41
  )
42
42
 
43
43
  def prepare(self, json: dict) -> 'ITDException':
44
- """Собрать ошибку для выброса - копию декларации, дополненную данными из ответа
45
-
46
- Args:
47
- json (dict): Тело ответа
48
-
49
- Returns:
50
- ITDException: Ошибка
51
- """
52
- # именно копию: декларации переиспользуются между запросами и потоками, мутировать их нельзя.
53
- # __init__ не вызываем - у наследников он с разными аргументами (NotFoundError('Post') и тд)
54
44
  exception = self.__class__.__new__(self.__class__)
55
45
  BaseException.__init__(exception, *self.args)
56
46
  exception.__dict__.update(self.__dict__)
@@ -58,7 +48,7 @@ class ITDException(Exception):
58
48
  return exception
59
49
 
60
50
  def _fill(self, error: dict) -> None:
61
- """Дополнить ошибку данными из ['error'] (пустой dict если там не объект)"""
51
+ pass
62
52
 
63
53
 
64
54
  class ValidateError(ITDException):
@@ -208,7 +198,7 @@ class InsufficientAuthLevelError(NoRightsError):
208
198
 
209
199
 
210
200
  class PinNotOwnedError(NoRightsError):
211
- code = "PIN_NOT_OWNED"
201
+ code = 'PIN_NOT_OWNED'
212
202
  text = 'You do not own this pin'
213
203
 
214
204
 
@@ -0,0 +1,74 @@
1
+ from pydantic import BaseModel
2
+
3
+ from itd.api.platform import get_announcements
4
+ from itd.core.base import ITDBaseModel
5
+ from itd.core.client import Client
6
+ from itd.enums import AnnouncementButtonStyle, AnnouncementButtonType
7
+
8
+
9
+ class AnnouncementImage(BaseModel):
10
+ url: str
11
+ width: int | None = None
12
+ height: int | None = None
13
+
14
+ def __str__(self):
15
+ return self.url
16
+
17
+
18
+ class AnnouncementButtonAction(BaseModel):
19
+ type: AnnouncementButtonType
20
+ url: str | None = None
21
+
22
+
23
+ class AnnouncementButton(BaseModel):
24
+ title: str
25
+ style: AnnouncementButtonStyle = AnnouncementButtonStyle.PRIMARY
26
+ action: AnnouncementButtonAction
27
+
28
+ def __str__(self):
29
+ return self.title
30
+
31
+
32
+ class Announcement(ITDBaseModel):
33
+ id: str
34
+ image: AnnouncementImage | None = None
35
+ title: str
36
+ description: str | None = None
37
+ additional_text: str | None = None
38
+
39
+ buttons: list[AnnouncementButton] = []
40
+
41
+ def __str__(self):
42
+ return self.title
43
+
44
+ def read(self):
45
+ assert self.client._session_file, 'No session file'
46
+ self.client._session_file.seen_announcements.append(self.id)
47
+ self.client._session_file.flush()
48
+
49
+
50
+ class Announcements(ITDBaseModel, list[Announcement]):
51
+ def __init__(self, *, hide_seen: bool = True, client: Client | None = None):
52
+ super().__init__(client=client)
53
+ self.load(hide_seen=hide_seen)
54
+
55
+ def load(self, *, hide_seen: bool = True) -> 'Announcements':
56
+ self.clear()
57
+ self.extend([Announcement.from_dict(announcement, client=self.client) for announcement in get_announcements(self.client).json()['announcements']])
58
+
59
+ if hide_seen and self.client._session_file:
60
+ for announcement in self.copy():
61
+ if announcement.id in self.client._session_file.seen_announcements:
62
+ self.remove(announcement)
63
+
64
+ return self
65
+
66
+ def get(self) -> Announcement | None:
67
+ if self:
68
+ return self[0]
69
+
70
+ @classmethod
71
+ def empty(cls):
72
+ instance = cls.__new__(cls)
73
+ super(Announcements, instance).__init__()
74
+ return instance
@@ -1,10 +1,7 @@
1
1
  from pydantic import BaseModel, Field
2
2
 
3
- from itd.api.etc import get_top_clans
3
+ from itd.api.users import get_top_clans
4
4
  from itd.core.base import ITDBaseModel
5
- from itd.core.logger import get_logger
6
-
7
- l = get_logger('clan') # noqa: E741
8
5
 
9
6
 
10
7
  class Clan(BaseModel):
@@ -27,10 +24,6 @@ class TopClans(ITDBaseModel, list[Clan]):
27
24
  self.extend([Clan.model_validate(clan) for clan in get_top_clans(self.client).json()['clans']])
28
25
  return self
29
26
 
30
- def refresh(self, *, client=None):
31
- l.warning('TopClans.refresh is deprecated and will be removed in 2.6.0. Please use TopClans.load')
32
- self.load()
33
-
34
27
  @classmethod
35
28
  def empty(cls):
36
29
  instance = cls.__new__(cls)
@@ -9,11 +9,11 @@ from pydantic import BeforeValidator, Field
9
9
 
10
10
  from itd.api.comments import add_comment, add_reply_comment, delete_comment, edit_comment, get_comments, get_replies, like_comment, unlike_comment
11
11
  from itd.core.base import ITDBaseModel, ITDList
12
- from itd.enums import CommentSorting, ReportReason, ReportTargetType
12
+ from itd.core.utils import parse_datetime, to_nullable_uuid, to_uuid
13
+ from itd.enums import CommentSorting, LoadStatus, ReportReason, ReportTargetType
13
14
  from itd.models.file import CommentAttach
14
15
  from itd.models.report import Report
15
16
  from itd.models.user import User
16
- from itd.core.utils import parse_datetime, to_nullable_uuid
17
17
  from itd.models.utils import ATTACHMENTS, format_attachments
18
18
 
19
19
  if TYPE_CHECKING:
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
22
22
 
23
23
 
24
24
  class Comment(ITDBaseModel):
25
- _refreshable = False
25
+ _refreshable = True
26
26
 
27
27
  id: UUID
28
28
  content: str
@@ -40,18 +40,30 @@ class Comment(ITDBaseModel):
40
40
 
41
41
  _post: Post | None = None
42
42
  _base_comment: Comment | None = None
43
+ _init_from_id: bool = False
44
+
45
+ def __init__(self, id: UUID | str, *, client: Client | None = None) -> None:
46
+ super().__init__(client=client)
47
+ self.id = to_uuid(id)
48
+ self._init_from_id = True
43
49
 
44
50
  @classmethod
45
51
  def from_dict(
46
52
  cls, data: dict, post: Post | None = None, base_comment: Comment | None = None, *, context: dict | None = None, client: Client | None = None
47
53
  ) -> Comment:
48
- context = dict(context or {}) # контекст родителя не выбрасываем, иначе комментарий потеряет его клиента
54
+ context = dict(context or {})
49
55
  if post is not None:
50
56
  context['post'] = post
51
57
  if base_comment is not None:
52
58
  context['base_comment'] = base_comment
53
59
  return super().from_dict(data, client=client, context=context)
54
60
 
61
+ def _refresh(self, *, client: Client):
62
+ if self._init_from_id:
63
+ raise RuntimeError('Unable to refresh comment that initialized by id. Please load comment from post')
64
+ else:
65
+ super()._refresh(client=client)
66
+
55
67
  @cached_property
56
68
  def replies(self) -> Replies:
57
69
  assert self._post is not None, 'post not set'
@@ -90,16 +102,17 @@ class Comment(ITDBaseModel):
90
102
  format_attachments(attachments)
91
103
  ).json(),
92
104
  post=self._post,
93
- base_comment=self,
105
+ base_comment=self._base_comment if self.is_reply else self,
94
106
  client=client or self.client
95
107
  )
108
+ comment.reply_to = User(user_id) if user_id else self.author # manual patch stupid api
96
109
  if self.is_reply:
97
110
  assert self._base_comment is not None
98
- self._base_comment.replies.insert(0, comment)
111
+ self._base_comment.replies.append(comment)
99
112
  self._base_comment.replies.total += 1
100
113
  self._base_comment.replies_count += 1
101
114
  else:
102
- self.replies.insert(0, comment)
115
+ self.replies.append(comment)
103
116
  self.replies.total += 1
104
117
  self.replies_count += 1
105
118
 
@@ -197,6 +210,7 @@ class Comment(ITDBaseModel):
197
210
  return not self.is_owner
198
211
 
199
212
  def _post_refresh(self, context: dict = {}):
213
+ self.load_status = LoadStatus.FULL
200
214
  self._base_comment = context.get('base_comment')
201
215
  self._post = context.get('post')
202
216
  for reply in self.first_replies:
@@ -257,7 +271,7 @@ class Replies(ITDList[Comment]):
257
271
  _base_comment: 'Comment'
258
272
 
259
273
  _post: Post
260
- total: int
274
+ total: int = 0
261
275
  cursor: int = 1
262
276
  _is_page_pagination = True
263
277
 
@@ -6,7 +6,8 @@ from uuid import UUID
6
6
 
7
7
  from pydantic import Field
8
8
 
9
- from itd.api.hashtags import get_hashtags, get_posts_by_hashtag, search_hashtags
9
+ from itd.api.hashtags import get_hashtags, get_posts_by_hashtag
10
+ from itd.api.search import search
10
11
  from itd.core.base import ITDBaseModel
11
12
  from itd.exceptions import NotFoundError
12
13
 
@@ -62,7 +63,7 @@ class Hashtags(ITDBaseModel, list[Hashtag]):
62
63
  def load(self, limit: int = 10):
63
64
  self.clear()
64
65
  if self.query:
65
- self.extend([Hashtag.from_dict(hashtag) for hashtag in search_hashtags(self.client, self.query, limit).json()['data']['hashtags']])
66
+ self.extend([Hashtag.from_dict(hashtag) for hashtag in search(self.client, self.query, 1, limit).json()['data']['hashtags']])
66
67
  else:
67
68
  self.extend([Hashtag.from_dict(hashtag) for hashtag in get_hashtags(self.client, limit).json()['data']['hashtags']])
68
69
  return self
@@ -3,10 +3,10 @@ from __future__ import annotations
3
3
  from datetime import datetime
4
4
  from json import loads
5
5
  from threading import Thread
6
- from typing import TYPE_CHECKING, Any, Callable, Iterator, Literal, cast
6
+ from typing import TYPE_CHECKING, Annotated, Any, Callable, Iterator, Literal, cast
7
7
  from uuid import UUID
8
8
 
9
- from pydantic import BaseModel, Field
9
+ from pydantic import BaseModel, BeforeValidator, Field
10
10
  from sseclient import SSEClient
11
11
 
12
12
  from itd.api.notifications import (
@@ -20,8 +20,9 @@ from itd.api.notifications import (
20
20
  )
21
21
  from itd.core.base import ITDBaseModel, ITDList
22
22
  from itd.core.client import Client
23
- from itd.enums import DebugResponseMode, LoadStatus, NotificationSubjectType, NotificationTargetType, NotificationType
24
23
  from itd.core.logger import get_logger
24
+ from itd.core.utils import parse_datetime
25
+ from itd.enums import DebugResponseMode, LoadStatus, NotificationSubjectType, NotificationTargetType, NotificationType
25
26
  from itd.models.user import User
26
27
 
27
28
  if TYPE_CHECKING:
@@ -150,8 +151,8 @@ class Notification(ITDBaseModel):
150
151
  )
151
152
 
152
153
  is_read: bool = Field(False, alias='read')
153
- read_at: datetime | None = Field(None, alias='readAt')
154
- created_at: datetime = Field(alias='createdAt')
154
+ read_at: Annotated[datetime, BeforeValidator(parse_datetime)] | None = Field(None, alias='readAt')
155
+ created_at: Annotated[datetime, BeforeValidator(parse_datetime)] = Field(alias='createdAt')
155
156
 
156
157
  actor: User
157
158
  sound: bool = False # for notifications from stream
@@ -64,7 +64,7 @@ class Post(ITDBaseModel):
64
64
  reposts_count: int = Field(0, alias='repostsCount')
65
65
  views_count: int = Field(0, alias='viewsCount')
66
66
 
67
- edited_at: datetime | None = Field(None, alias='editedAt')
67
+ edited_at: Annotated[datetime, BeforeValidator(parse_datetime)] | None = Field(None, alias='editedAt')
68
68
 
69
69
  is_liked: bool = Field(False, alias='isLiked')
70
70
  is_reposted: bool = Field(False, alias='isReposted')
@@ -147,7 +147,13 @@ class Post(ITDBaseModel):
147
147
 
148
148
  @classmethod
149
149
  def from_dict(
150
- cls, data: dict, source: ViewSource = ViewSource.POST_PAGE, source_context: str | None = None, *, context: dict | None = None, client: Client | None = None
150
+ cls,
151
+ data: dict,
152
+ source: ViewSource = ViewSource.POST_PAGE,
153
+ source_context: str | None = None,
154
+ *,
155
+ context: dict | None = None,
156
+ client: Client | None = None
151
157
  ) -> 'Post':
152
158
  context = dict(context or {})
153
159
  context.update({'source': source, 'source_context': source_context})
@@ -399,7 +405,7 @@ class Post(ITDBaseModel):
399
405
  self._set_stats(stats[0])
400
406
 
401
407
  def _set_stats(self, stats: dict):
402
- fields = {value.alias or name: name for name, value in self._validator.model_fields.items()}
408
+ fields = {value.alias or name: name for name, value in self._pydantic_model.model_fields.items()}
403
409
  for name, value in stats.items():
404
410
  if name in fields:
405
411
  setattr(self, fields[name], value)
@@ -1,13 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
- from typing import TYPE_CHECKING
4
+ from typing import TYPE_CHECKING, Annotated
5
5
  from uuid import UUID
6
6
 
7
- from pydantic import Field
7
+ from pydantic import BeforeValidator, Field
8
8
 
9
9
  from itd.api.reports import report
10
10
  from itd.core.base import ITDBaseModel
11
+ from itd.core.utils import parse_datetime
11
12
  from itd.enums import ReportReason, ReportTargetType
12
13
 
13
14
  if TYPE_CHECKING:
@@ -18,7 +19,7 @@ class Report(ITDBaseModel):
18
19
  _refreshable = False
19
20
 
20
21
  id: UUID
21
- created_at: datetime = Field(alias='createdAt')
22
+ created_at: Annotated[datetime, BeforeValidator(parse_datetime)] = Field(alias='createdAt')
22
23
 
23
24
  def __init__(self, target_id: UUID, target_type: ReportTargetType, reason: ReportReason, description: str | None = None, client: Client | None = None):
24
25
  super().__init__(client)
@@ -1,12 +1,14 @@
1
1
  from datetime import datetime
2
2
  from ipaddress import IPv4Address
3
+ from typing import Annotated
3
4
  from uuid import UUID
4
5
 
5
- from pydantic import Field
6
+ from pydantic import BeforeValidator, Field
6
7
 
7
8
  from itd.api.sessions import get_sessions, revoke, revoke_all
8
9
  from itd.core.base import ITDBaseModel
9
10
  from itd.core.client import Client
11
+ from itd.core.utils import parse_datetime
10
12
  from itd.enums import DeviceType
11
13
 
12
14
 
@@ -16,9 +18,9 @@ class Session(ITDBaseModel):
16
18
  id: UUID
17
19
  is_current: bool = Field(alias='isCurrent')
18
20
 
19
- created_at: datetime = Field(alias='createdAt')
20
- last_used_at: datetime = Field(alias='lastUsedAt')
21
- expires_at: datetime = Field(alias='expiresAt')
21
+ created_at: Annotated[datetime, BeforeValidator(parse_datetime)] = Field(alias='createdAt')
22
+ last_used_at: Annotated[datetime, BeforeValidator(parse_datetime)] = Field(alias='lastUsedAt')
23
+ expires_at: Annotated[datetime, BeforeValidator(parse_datetime)] = Field(alias='expiresAt')
22
24
 
23
25
  ip: IPv4Address = Field(alias='ipAddress')
24
26
  country: str | None = Field(None, alias='ipCountry') # country code
@@ -2,14 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
4
  from functools import cached_property
5
- from typing import TYPE_CHECKING, overload
5
+ from typing import TYPE_CHECKING, Annotated, overload
6
6
  from uuid import UUID
7
7
 
8
- from pydantic import BaseModel, Field
8
+ from pydantic import BaseModel, BeforeValidator, Field
9
9
 
10
- from itd.core.default import get_default_client
11
- from itd.api.etc import get_who_to_follow
12
10
  from itd.api.pins import get_pins, remove_pin
11
+ from itd.api.search import search
13
12
  from itd.api.subscription import get_payment_methods, get_subscription, pay_subscription, toggle_subscription_auto_renewal
14
13
  from itd.api.users import (
15
14
  block,
@@ -21,8 +20,8 @@ from itd.api.users import (
21
20
  get_privacy,
22
21
  get_profile,
23
22
  get_user,
23
+ get_who_to_follow,
24
24
  restore_account,
25
- search_users,
26
25
  unblock,
27
26
  unfollow,
28
27
  update_privacy,
@@ -30,13 +29,14 @@ from itd.api.users import (
30
29
  )
31
30
  from itd.api.users import get_follow_status as _get_follow_status
32
31
  from itd.core.base import ITDBaseModel, ITDList
32
+ from itd.core.default import get_default_client
33
+ from itd.core.utils import parse_datetime, to_uuid
33
34
  from itd.enums import AccessType, LastSeenUnit, LoadStatus, ReportReason, ReportTargetType, Role, Unset
34
35
  from itd.exceptions import AccountDeletedError, NotFoundError, PinNotOwnedError
35
36
  from itd.models.pin import Pin
36
37
  from itd.models.poll import NewPoll
37
38
  from itd.models.report import Report
38
39
  from itd.models.span import Span
39
- from itd.core.utils import parse_datetime, to_uuid
40
40
  from itd.models.utils import ATTACHMENTS
41
41
 
42
42
  if TYPE_CHECKING:
@@ -124,7 +124,7 @@ class Subscription(ITDBaseModel):
124
124
  _refreshable = False
125
125
 
126
126
  active: bool = Field(False, alias='isActive')
127
- expires_at: datetime | None = Field(None, alias='expiresAt')
127
+ expires_at: Annotated[datetime, BeforeValidator(parse_datetime)] | None = Field(None, alias='expiresAt')
128
128
  auto_renewal: bool = Field(True, alias='autoRenewal')
129
129
 
130
130
  def _refresh(self, *, client: Client): # refreshes only is_active
@@ -231,7 +231,7 @@ class User(_UserBase):
231
231
  False, alias='isBlockedByThem'
232
232
  ) # not 100% true, server returns value only if isBlockedByThem is True and isBlockedByMe is False
233
233
  is_blocking: bool = Field(False, alias='isBlockedByMe')
234
- blocked_at: datetime | None = Field(None, alias='blockedAt')
234
+ blocked_at: Annotated[datetime, BeforeValidator(parse_datetime)] | None = Field(None, alias='blockedAt')
235
235
 
236
236
  followers_count: int | None = Field(None, alias='followersCount')
237
237
  following_count: int | None = Field(None, alias='followingCount')
@@ -240,14 +240,15 @@ class User(_UserBase):
240
240
  wall_access: AccessType | None = Field(None, alias='wallAccess') # none if blocked
241
241
  likes_visibility: AccessType | None = Field(None, alias='likesVisibility') # none if blocked
242
242
  is_private: bool | None = Field(None, alias='isPrivate') # none if following or blocked
243
+ can_message: bool = Field(False, alias='canMessage')
243
244
 
244
245
  is_subscribed: bool = Field(False, alias='hasNuksta')
245
- last_seen: datetime | LastSeen | None = Field(None, alias='lastSeen') # none if hidden or blocked
246
+ last_seen: LastSeen | None = Field(None, alias='lastSeen') # none if hidden or blocked
246
247
  online: bool = False
247
248
 
248
249
  pinned_post_id: UUID | None = Field(None, alias='pinnedPostId') # none if no or blocked
249
250
 
250
- created_at: datetime | None = Field(None, alias='createdAt') # none if blocked
251
+ created_at: Annotated[datetime, BeforeValidator(parse_datetime)] | None = Field(None, alias='createdAt') # none if blocked
251
252
 
252
253
  def _post_refresh(self, context: dict = {}):
253
254
  if 'id' in self.__dict__:
@@ -370,7 +371,7 @@ class User(_UserBase):
370
371
  def following(self) -> list[User]:
371
372
  return [User.from_dict(user, client=self.client) for user in get_following(self.client, self._identifier).json()['data']['users']]
372
373
 
373
- @property
374
+ @cached_property
374
375
  def followers(self) -> list[User]:
375
376
  return [User.from_dict(user, client=self.client) for user in get_followers(self.client, self._identifier).json()['data']['users']]
376
377
 
@@ -396,7 +397,7 @@ class Me(_UserBase):
396
397
  following_count: int = Field(alias='followingCount')
397
398
  posts_count: int = Field(alias='postsCount')
398
399
 
399
- created_at: datetime = Field(alias='createdAt')
400
+ created_at: Annotated[datetime, BeforeValidator(parse_datetime)] = Field(alias='createdAt')
400
401
 
401
402
  def __init__(self, client: Client | None = None) -> None:
402
403
  super().__init__('me', client)
@@ -573,7 +574,7 @@ class Users(ITDBaseModel, list[User]):
573
574
 
574
575
  def load(self, limit: int = 10):
575
576
  self.clear()
576
- self.extend([User.from_dict(hashtag) for hashtag in search_users(self.client, self.query, limit).json()['data']['users']])
577
+ self.extend([User.from_dict(hashtag) for hashtag in search(self.client, self.query, limit, 1).json()['data']['users']])
577
578
  return self
578
579
 
579
580
  @classmethod
@@ -14,7 +14,7 @@ version_provider = "uv"
14
14
 
15
15
  [project]
16
16
  name = "itd-sdk"
17
- version = "2.8.0"
17
+ version = "2.8.2"
18
18
  description = "ITD client for python"
19
19
  readme = "README.md"
20
20
  authors = [
@@ -36,6 +36,7 @@ dependencies = [
36
36
  "telegramify-markdown>=0.5.4",
37
37
  "lxml>=6.1.1",
38
38
  "platformdirs>=4.10.0",
39
+ "websockets>=16.1.1",
39
40
  ]
40
41
  requires-python = ">=3.10"
41
42
 
@@ -1,16 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from itd.core.request import endpoint
6
-
7
- if TYPE_CHECKING:
8
- from itd.core.client import Client
9
-
10
-
11
- @endpoint('get', 'users/stats/top-clans')
12
- def get_top_clans(client: Client): ...
13
-
14
-
15
- @endpoint('get', 'users/suggestions/who-to-follow')
16
- def get_who_to_follow(client: Client): ...
@@ -1,43 +0,0 @@
1
- from datetime import datetime
2
- from json import dumps, loads
3
- from pathlib import Path
4
-
5
- from pydantic import BaseModel, Field
6
-
7
- # class CredfileSession(BaseModel):
8
- # id: int
9
- # last_active: datetime
10
- # main: bool
11
-
12
- # @classmethod
13
- # def current(cls, main: bool = False):
14
- # return cls(id=getpid(), last_active=datetime.now(), main=main)
15
-
16
-
17
- class Credfile(BaseModel):
18
- access: str | None = None
19
- refresh: str | None = None
20
- valid: bool = True
21
- updated_at: datetime = Field(default_factory=datetime.now)
22
- # sessions: list[CredfileSession] = Field(default_factory=lambda: [])
23
- _file: Path
24
-
25
- def flush(self):
26
- self.updated_at = datetime.now()
27
- self._file.write_text(dumps(self.model_dump(mode='json')))
28
-
29
- def update(self):
30
- new = self.model_validate(loads(self._file.read_text()))
31
- if self.updated_at <= new.updated_at:
32
- for field in Credfile.model_fields:
33
- setattr(self, field, getattr(new, field))
34
- return True
35
- return False
36
-
37
- # @field_validator('sessions', mode='after')
38
- # def validate_sessions(self, sessions: list[CredfileSession]):
39
- # for session in sessions.copy():
40
- # if session.last_active < datetime.now() - timedelta(minutes=30):
41
- # sessions.remove(session)
42
-
43
- # sessions.append(CredfileSession.current(main=not any(session.main for session in sessions)))
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