itd-sdk 2.4.0__tar.gz → 2.4.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 (43) hide show
  1. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/PKG-INFO +1 -1
  2. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/sessions.py +2 -2
  3. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/base.py +35 -36
  4. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/clan.py +2 -2
  5. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/client.py +2 -1
  6. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/comment.py +2 -2
  7. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/enums.py +6 -2
  8. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/file.py +20 -20
  9. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/hashtag.py +3 -4
  10. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/notification.py +2 -2
  11. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/post.py +21 -29
  12. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/report.py +2 -4
  13. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/session.py +14 -2
  14. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/user.py +21 -26
  15. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/pyproject.toml +58 -58
  16. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/README.md +0 -0
  17. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/__init__.py +0 -0
  18. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/_default.py +0 -0
  19. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/__init__.py +0 -0
  20. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/auth.py +0 -0
  21. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/comments.py +0 -0
  22. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/dwell.py +0 -0
  23. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/etc.py +0 -0
  24. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/files.py +0 -0
  25. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/hashtags.py +0 -0
  26. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/notifications.py +0 -0
  27. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/pins.py +0 -0
  28. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/platform.py +0 -0
  29. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/polls.py +0 -0
  30. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/posts.py +0 -0
  31. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/reports.py +0 -0
  32. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/search.py +0 -0
  33. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/subscription.py +0 -0
  34. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/users.py +0 -0
  35. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/verification.py +0 -0
  36. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/exceptions.py +0 -0
  37. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/logger.py +0 -0
  38. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/pin.py +0 -0
  39. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/poll.py +0 -0
  40. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/request.py +0 -0
  41. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/span.py +0 -0
  42. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/utils.py +0 -0
  43. {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itd-sdk
3
- Version: 2.4.0
3
+ Version: 2.4.1
4
4
  Summary: ITD client for python
5
5
  Keywords: itd,itdsocial
6
6
  Author: firedotguy
@@ -14,10 +14,10 @@ def get_sessions(client: Client):
14
14
 
15
15
  @catch_errors()
16
16
  @rate_limit()
17
- def delete_session(client: Client, id: UUID):
17
+ def revoke(client: Client, id: UUID):
18
18
  return client.request('delete', f'v1/auth/sessions/{id}')
19
19
 
20
20
  @catch_errors()
21
21
  @rate_limit()
22
- def delete_all_sessions(client: Client):
22
+ def revoke_all(client: Client):
23
23
  return client.request('delete', 'v1/auth/sessions')
@@ -16,7 +16,7 @@ from itd.exceptions import (
16
16
  ITDException, ValidationError, RateLimitError, UnauthorizedError, AccessTokenExpiredError,
17
17
  DEFAULT_ERRORS, AccountDeletedError
18
18
  )
19
- from itd.enums import All, ALL, DebugResponseMode, RateLimitMode, BATCH, Batch
19
+ from itd.enums import All, ALL, DebugResponseMode, RateLimitMode, BATCH, Batch, LoadStatus
20
20
  if TYPE_CHECKING:
21
21
  from itd.client import Client
22
22
 
@@ -47,8 +47,7 @@ def _field_has_default(cls: type, name: str) -> bool:
47
47
  class ITDBaseModel:
48
48
  """Базовый класс модельки"""
49
49
  _refreshable: bool = True
50
- _loaded: bool = False
51
- _loading: bool = False
50
+ load_status: LoadStatus = LoadStatus.NO
52
51
  _load_with_parent: bool = True # load parent model if model called
53
52
  _validator: Callable[[Any], type[BaseModel]] | None = None # callable (pls use lambda), becuase we havent validator at that moment (it depends on this class)
54
53
 
@@ -61,16 +60,39 @@ class ITDBaseModel:
61
60
  value._client = client
62
61
  object.__setattr__(self, name, value)
63
62
 
64
- def _post_refresh(self):
65
- pass
63
+ def _post_refresh(self): ...
66
64
 
67
65
  @property
68
66
  def client(self) -> Client:
69
67
  return self._client
70
68
 
71
- def refresh(self) -> Any:
72
- l.warning('refresh is not implemented but have called')
73
- self._loaded = True
69
+ def _refresh(self, *, client: Client) -> dict: ...
70
+
71
+ def refresh(self, *, client: Client | None = None) -> Any:
72
+ self.load_status = LoadStatus.LOADING
73
+ self._fill_from_data(self._refresh(client=client or self.client))
74
+ self.load_status = LoadStatus.FULL
75
+ return self
76
+
77
+
78
+ def _fill_from_data(self, data: dict):
79
+ assert self._validator, 'Unable to use fill_from_data without a validator'
80
+ validated = self._validator().model_validate(data) # pyright: ignore[reportCallIssue]
81
+ self._fields_from_data = validated.model_fields_set
82
+ for name, value in validated.__dict__.items():
83
+ setattr(self, name, value)
84
+
85
+ self._post_refresh()
86
+
87
+
88
+ @classmethod
89
+ def from_dict(cls, data: dict, *, client: Client | None = None):
90
+ instance = cls.__new__(cls)
91
+ ITDBaseModel.__init__(instance, client)
92
+
93
+ instance._fill_from_data(data)
94
+ instance.load_status = LoadStatus.PARTIALLY
95
+ return instance
74
96
 
75
97
 
76
98
  if not TYPE_CHECKING:
@@ -78,20 +100,20 @@ class ITDBaseModel:
78
100
  value = _getattr(self, name)
79
101
  if (
80
102
  name.startswith('_') or # приватный аттрибут
81
- name in ('client', 'model_fields_set') or
103
+ name in ('client', 'model_fields_set', 'load_status') or
82
104
  not _getattr(self, '_refreshable') or # не рефрешабельная модель
83
105
  not _getattr(self, 'client').config.auto_load or # отключено в конфиге
84
106
  callable(value) or # функция
107
+ self.load_status == LoadStatus.LOADING or
85
108
  (not _getattr(self, 'client').config.load_comments_from_post and _is_comments_on_post(value, self)) or
86
109
  (isinstance(value, ITDBaseModel) and not value._load_with_parent)
87
110
  ):
88
111
  # l.debug('return %s as is', name)
89
112
  return object.__getattribute__(self, name)
90
113
 
91
- if isinstance(value, FieldInfo) or (isinstance(value, ITDBaseModel) and value._load_with_parent and not value._loaded) or (not _getattr(self, '_loaded') and value is None):
92
- l.info('refresh %s (caused by %s)', self.__class__.__name__, name)
114
+ if isinstance(value, FieldInfo) or (self.load_status != LoadStatus.FULL and name not in self._fields_from_data):
115
+ l.info('refresh %s field=%s load_status=%s', self.__class__.__name__, name, self.load_status.value)
93
116
  self.refresh()
94
- return object.__getattribute__(self, name)
95
117
 
96
118
  return object.__getattribute__(self, name)
97
119
 
@@ -179,7 +201,7 @@ class ITDList[T](ITDBaseModel, list[T]):
179
201
  def _get_objects(data: dict) -> list[dict]:
180
202
  return []
181
203
 
182
- def refresh(self, count: int | All | Batch | None = None, limit: int | Batch = BATCH, client: Client | None = None) -> list[T]:
204
+ def refresh(self, count: int | All | Batch | None = None, limit: int | Batch = BATCH, *, client: Client | None = None) -> list[T]:
183
205
  """Обновить список (удалить все элементы и загрузить заново)
184
206
 
185
207
  Args:
@@ -265,29 +287,6 @@ class ITDList[T](ITDBaseModel, list[T]):
265
287
  return self.load_all()
266
288
 
267
289
 
268
- def refresh_wrapper(func):
269
- """Декоратор для self.refresh"""
270
- @wraps(func)
271
- def wrapper(self, client: Client | None = None):
272
- # if self._loading:
273
- # return
274
-
275
- # self._loading = True
276
- data = func(self, client or self.client)
277
- if self._validator:
278
- validated = self._validator().model_validate(data)
279
- self._fields_from_data = validated.model_fields_set
280
- l.debug('fill %s', self.__class__.__name__)
281
- for name, value in validated.__dict__.items():
282
- setattr(self, name, value)
283
-
284
- # self._loading = False
285
- self._loaded = True
286
- self._post_refresh()
287
-
288
- return wrapper
289
-
290
-
291
290
  def _filter_bytes(args: tuple):
292
291
  filtered = []
293
292
  for arg in args:
@@ -27,8 +27,8 @@ class TopClans(ITDBaseModel, list[Clan]):
27
27
  self.extend([Clan.model_validate(clan) for clan in get_top_clans(self.client).json()['clans']])
28
28
  return self
29
29
 
30
- def refresh(self):
31
- l.warning('TopClans.refresh is deprecated and will be removed in 2.6.0. Pleas use TopClans.load')
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
32
  self.load()
33
33
 
34
34
  @classmethod
@@ -75,6 +75,7 @@ class Config:
75
75
  dwell_send_interval: float = 2
76
76
  dwell_save_on_quit: bool = True
77
77
  dwell_wait_durations: bool = True
78
+ post_view_increment: bool = False
78
79
 
79
80
  post_update_stats: bool = False
80
81
  post_update_stats_interval: int = 3
@@ -285,7 +286,7 @@ class Client:
285
286
  tuple[list[User], list[Hashtag]]: Результат поиска
286
287
  """
287
288
  res = search(self, query, users_limit, hashtags_limit).json()['data']
288
- return [User._from_dict(user, False, self) for user in res['users']], [Hashtag._from_dict(hashtag, self) for hashtag in res['hashtags']]
289
+ return [User._from_dict(user, client=self) for user in res['users']], [Hashtag._from_dict(hashtag, self) for hashtag in res['hashtags']]
289
290
 
290
291
  def search_users(self, query: str, limit: int = 20) -> list[User]:
291
292
  """Поиск пользователей
@@ -152,12 +152,12 @@ class _CommentValidate(BaseModel, Comment):
152
152
  @classmethod
153
153
  def validate_reply_to(cls, reply_to: dict | None):
154
154
  if reply_to is not None:
155
- return User._from_dict(reply_to, False)
155
+ return User._from_dict(reply_to)
156
156
 
157
157
  @field_validator('author', mode='plain')
158
158
  @classmethod
159
159
  def validate_author(cls, author: dict):
160
- return User._from_dict(author, False)
160
+ return User._from_dict(author)
161
161
 
162
162
 
163
163
 
@@ -147,7 +147,6 @@ class AccessType(Enum):
147
147
 
148
148
  _ACCESS_TYPE_HIERARCHY = [AccessType.EVERYONE, AccessType.FOLLOWERS, AccessType.MUTUAL, AccessType.EVERYONE] # 100% that hierarcy is spelled wrong
149
149
 
150
-
151
150
  class SpanType(Enum):
152
151
  MONOSPACE = 'monospace' # моноширный (код)
153
152
  STRIKE = 'strike' # зачеркнутый
@@ -160,11 +159,16 @@ class SpanType(Enum):
160
159
  QUOTE = 'quote' # цитата (не работает)
161
160
  MENTION = 'mention' # упоминание (появляется только при получении постов, при создании нету)
162
161
 
163
-
164
162
  class Role(Enum):
165
163
  USER = 'user'
166
164
  ADMIN = 'admin'
167
165
 
166
+ class LoadStatus(Enum):
167
+ NO = 'no'
168
+ LOADING = 'loading'
169
+ PARTIALLY = 'partially'
170
+ FULL = 'full'
171
+
168
172
 
169
173
  class Unset: pass
170
174
  UNSET = Unset()
@@ -8,7 +8,7 @@ from pathlib import Path
8
8
  from pydantic import BaseModel, Field
9
9
  from requests import get
10
10
 
11
- from itd.base import ITDBaseModel, refresh_wrapper
11
+ from itd.base import ITDBaseModel
12
12
  from itd.enums import AttachType
13
13
  from itd.api.files import upload_file, delete_file
14
14
  if TYPE_CHECKING:
@@ -29,37 +29,37 @@ class File(ITDBaseModel):
29
29
  def __init__(self, name: str, data: bytes | BufferedReader, client: Client | None = None):
30
30
  super().__init__(client)
31
31
  self.filename = name
32
- self._upload(data)
32
+ self.data = data
33
+ self.refresh()
33
34
 
34
35
  @classmethod
35
- def from_path(cls, path: Path | str):
36
+ def from_path(cls, path: Path | str, *, client: Client | None = None):
36
37
  if isinstance(path, str):
37
38
  path = Path(path)
38
- return cls(path.name, path.read_bytes())
39
+ return cls(path.name, path.read_bytes(), client)
39
40
 
40
41
  @classmethod
41
- def from_bytes(cls, data: bytes | BufferedReader):
42
- try:
43
- from filetype import guess
44
- except ModuleNotFoundError:
45
- raise ImportError('filetype is required for File.from_bytes. Install by running "uv add itd-sdk[filetype]" (or "pip install itd-sdk[filetype]" if you are using pip)')
42
+ def from_bytes(cls, data: bytes | BufferedReader, name: str | None = None, *, client: Client | None = None):
43
+ if not name:
44
+ try:
45
+ from filetype import guess
46
+ except ModuleNotFoundError:
47
+ raise ImportError('filetype is required for File.from_bytes. Install by running "uv add itd-sdk[filetype]" (or "pip install itd-sdk[filetype]" if you are using pip)')
46
48
 
47
- kind = guess(data)
48
- return cls(
49
- f'file.{kind.extension}' if kind else 'file.0',
50
- data
51
- )
49
+ kind = guess(data)
50
+ name = f'file.{kind.extension}' if kind else 'file.0'
52
51
 
53
- @refresh_wrapper
54
- def _upload(self, data: bytes | BufferedReader):
55
- return upload_file(self.client, self.filename, data).json()
52
+ return cls(name, data, client)
56
53
 
57
- def delete(self) -> None:
58
- delete_file(self.client, self.id)
54
+ def _refresh(self, *, client: Client):
55
+ return upload_file(client, self.filename, self.data).json()
56
+
57
+ def delete(self, *, client: Client | None = None) -> None:
58
+ delete_file(client or self.client, self.id)
59
59
 
60
60
  def download(self, name: str | None = None) -> None:
61
61
  with open(name or self.filename, 'wb') as fl:
62
- fl.write(get(self.url, timeout=60).content)
62
+ fl.write(get(self.url, timeout=self.client.config.timeout_file).content)
63
63
 
64
64
  def __str__(self) -> str:
65
65
  return self.filename
@@ -5,7 +5,7 @@ from uuid import UUID
5
5
  from pydantic import BaseModel, Field
6
6
 
7
7
  from itd.api.hashtags import get_hashtags, get_posts_by_hashtag
8
- from itd.base import ITDBaseModel, refresh_wrapper
8
+ from itd.base import ITDBaseModel
9
9
  if TYPE_CHECKING:
10
10
  from itd.client import Client
11
11
  from itd.post import HashtagPosts
@@ -22,9 +22,8 @@ class Hashtag(ITDBaseModel):
22
22
  super().__init__(client)
23
23
  self.name = name.lstrip('#')
24
24
 
25
- @refresh_wrapper
26
- def refresh(self, client: Client | None = None):
27
- return get_posts_by_hashtag(client or self.client, self.name, limit=1).json()['data']['hashtag']
25
+ def _refresh(self, *, client: Client):
26
+ return get_posts_by_hashtag(client, self.name, limit=1).json()['data']['hashtag']
28
27
 
29
28
  @classmethod
30
29
  def _from_dict(cls, data: dict, client: Client | None = None):
@@ -10,7 +10,7 @@ from sseclient import SSEClient
10
10
 
11
11
  from itd.base import ITDBaseModel, ITDList
12
12
  from itd.client import Client
13
- from itd.enums import NotificationTargetType, NotificationType, All, ALL
13
+ from itd.enums import NotificationTargetType, NotificationType
14
14
  from itd.user import User
15
15
  from itd.api.notifications import (
16
16
  mark_as_read, mark_all_as_read, get_notifications, get_unread_notifications_count,
@@ -109,7 +109,7 @@ class _NotificationValidate(BaseModel, Notification):
109
109
  @field_validator('actor', mode='plain')
110
110
  @classmethod
111
111
  def validate_actor(cls, actor: dict):
112
- return User._from_dict(actor, False)
112
+ return User._from_dict(actor)
113
113
 
114
114
 
115
115
 
@@ -24,8 +24,8 @@ from itd.api.posts import (
24
24
  )
25
25
  from itd.api.hashtags import get_posts_by_hashtag
26
26
  from itd.api.dwell import send_views, send_interactions
27
- from itd.base import ITDBaseModel, refresh_wrapper, ITDList
28
- from itd.enums import PostsTab, UserPostSorting, ReportReason, ReportTargetType, ParseMode, ALL, ViewReason, ViewSource, InteractionType
27
+ from itd.base import ITDBaseModel, ITDList
28
+ from itd.enums import PostsTab, UserPostSorting, ReportReason, ReportTargetType, ParseMode, ALL, ViewReason, ViewSource, InteractionType, LoadStatus
29
29
  from itd.exceptions import NotFoundError
30
30
  from itd.logger import get_logger
31
31
  from itd.utils import to_uuid, parse_datetime, format_attachments, ATTACHMENTS, parse_html, parse_md, calc_view_duration
@@ -66,6 +66,8 @@ class ViewEvent(DwellEvent):
66
66
 
67
67
 
68
68
  class DwellTracker(ITDBaseModel):
69
+ _refreshable = False
70
+
69
71
  def __init__(self, client: Client | None = None) -> None:
70
72
  super().__init__(client)
71
73
  self.views: list[ViewEvent] = []
@@ -313,25 +315,16 @@ class Post(ITDBaseModel):
313
315
  for name, value in validated.__dict__.items():
314
316
  setattr(instance, name, value)
315
317
 
316
- instance._loaded = False
318
+ instance.load_status = LoadStatus.PARTIALLY
317
319
  instance._post_refresh()
318
320
 
319
321
  return instance
320
322
 
321
323
  @classmethod
322
- def _from_dict(cls, data: dict, source: ViewSource = ViewSource.POST_PAGE, source_context: str | None = None, set_loaded: bool = True, client: Client | None = None) -> 'Post':
323
- instance = cls.__new__(cls)
324
- super(Post, instance).__init__(client)
325
-
326
- validated = _PostValidate.model_validate(data)
327
- instance._fields_from_data = validated.model_fields_set
328
- for name, value in validated.__dict__.items():
329
- setattr(instance, name, value)
330
-
331
- instance._loaded = set_loaded
324
+ def from_dict(cls, data: dict, source: ViewSource = ViewSource.POST_PAGE, source_context: str | None = None, *, client: Client | None = None) -> 'Post':
325
+ instance = super().from_dict(data)
332
326
  instance.source = source
333
327
  instance.source_context = source_context
334
- instance._post_refresh()
335
328
  return instance
336
329
 
337
330
 
@@ -339,9 +332,8 @@ class Post(ITDBaseModel):
339
332
  assert self.poll, 'No poll'
340
333
  self.poll.vote(options, client or self.client)
341
334
 
342
- @refresh_wrapper
343
- def refresh(self, client: Client | None = None):
344
- return get_post(client or self.client, self.id).json()['data']
335
+ def _refresh(self, *, client: Client):
336
+ return get_post(client, self.id).json()['data']
345
337
 
346
338
 
347
339
  def __str__(self) -> str:
@@ -413,11 +405,12 @@ class Post(ITDBaseModel):
413
405
  Post: Пост
414
406
  """
415
407
  post = repost(client or self.client, self.id, content).json()
416
- self.reposts_count += 1
408
+ if 'reposts_count' in self._fields_from_data:
409
+ self.reposts_count += 1
417
410
  if (client or self.client) == self.client:
418
411
  self.is_reposted = True
419
412
 
420
- return Post._from_dict(post, client=client)
413
+ return Post.from_dict(post, client=client)
421
414
 
422
415
  def view(self, client: Client | None = None, entered_at: datetime | None = None, duration: int | None = None, reason: ViewReason = ViewReason.NORMAL) -> None:
423
416
  """Просмотреть пост
@@ -427,9 +420,6 @@ class Post(ITDBaseModel):
427
420
  """
428
421
  c = client or self.client
429
422
  if c.dwell_tracker is not None:
430
- if self.vs is None:
431
- self.refresh(c)
432
- assert self.vs
433
423
  if duration is None:
434
424
  duration = calc_view_duration(c.config, self.content, self.attachments)
435
425
  if c.config.dwell_wait_durations:
@@ -439,7 +429,8 @@ class Post(ITDBaseModel):
439
429
  view_post(c, self.id)
440
430
  if c == self.client:
441
431
  self.is_viewed = True
442
- # post can be already viewed, so view will not add; thats why do not change views_count
432
+ if c.config.post_view_increment and 'views_count' in self._fields_from_data:
433
+ self.views_count += 1
443
434
 
444
435
  def pin(self, client: Client | None = None) -> None:
445
436
  """Закрепить пост
@@ -514,7 +505,8 @@ class Post(ITDBaseModel):
514
505
  Comment: Комментарий
515
506
  """
516
507
  comment = self.comments.new(content, attachments, client or self.client)
517
- self.comments_count += 1
508
+ if 'comments_count' in self._fields_from_data:
509
+ self.comments_count += 1
518
510
  return comment
519
511
 
520
512
  def report(self, reason: ReportReason, description: str | None = None, client: Client | None = None) -> Report:
@@ -575,7 +567,7 @@ class _PostValidate(BaseModel, Post): # BaseModel MUST be first or you ll have s
575
567
  def validate_original_post(cls, post: dict | None = None):
576
568
  if post is None:
577
569
  return
578
- return Post._from_dict(post, set_loaded=False)
570
+ return Post.from_dict(post)
579
571
 
580
572
  @field_validator('poll', mode='plain')
581
573
  @classmethod
@@ -596,13 +588,13 @@ class _PostValidate(BaseModel, Post): # BaseModel MUST be first or you ll have s
596
588
  return None
597
589
  if isinstance(author, _UserBase):
598
590
  return author
599
- return User._from_dict(author, False)
591
+ return User._from_dict(author)
600
592
 
601
593
  @field_validator('wall_recipient', mode='plain')
602
594
  @classmethod
603
595
  def validate_wall_recipient(cls, wall_recipient: dict | None):
604
596
  if wall_recipient is not None:
605
- return User._from_dict(wall_recipient, False)
597
+ return User._from_dict(wall_recipient)
606
598
 
607
599
 
608
600
 
@@ -624,7 +616,7 @@ class _BasePosts(ITDList[Post]):
624
616
  return data['posts']
625
617
 
626
618
  def _extend(self, objects: list, client: Client):
627
- self.extend([Post._from_dict(post, self.source, self.source_context, client=client) for post in objects])
619
+ self.extend([Post.from_dict(post, self.source, self.source_context, client=client) for post in objects])
628
620
 
629
621
  def __setattr__(self, name: str, value) -> None:
630
622
  if name == '_client':
@@ -766,7 +758,7 @@ class HashtagPosts(_BasePosts):
766
758
  return get_posts_by_hashtag(client, self.hashtag.name, self.cursor, limit).json()['data']
767
759
 
768
760
  def _extend(self, objects: list, client: Client):
769
- self.extend([Post._from_dict(post, self.source, self.source_context, set_loaded=False, client=client) for post in objects])
761
+ self.extend([Post.from_dict(post, self.source, self.source_context, client=client) for post in objects])
770
762
 
771
763
  def _get_total(self, data: dict):
772
764
  return data['hashtag']['postsCount']
@@ -5,7 +5,7 @@ from datetime import datetime
5
5
 
6
6
  from pydantic import BaseModel, Field
7
7
 
8
- from itd.base import ITDBaseModel, refresh_wrapper
8
+ from itd.base import ITDBaseModel
9
9
  from itd.enums import ReportReason, ReportTargetType
10
10
  from itd.api.reports import report
11
11
  if TYPE_CHECKING:
@@ -28,9 +28,7 @@ class Report(ITDBaseModel):
28
28
 
29
29
  self.refresh()
30
30
 
31
-
32
- @refresh_wrapper
33
- def refresh(self, client: Client | None = None):
31
+ def _refresh(self, *, client: Client):
34
32
  return report(client or self.client, self.target_id, self.target_type, self.reason, self.description).json()['data']
35
33
 
36
34
 
@@ -7,7 +7,7 @@ from pydantic import Field, BaseModel
7
7
  from itd.base import ITDBaseModel
8
8
  from itd.client import Client
9
9
  from itd.enums import DeviceType
10
- from itd.api.sessions import get_sessions, delete_session, delete_all_sessions
10
+ from itd.api.sessions import get_sessions, revoke, revoke_all
11
11
 
12
12
 
13
13
  class Session(ITDBaseModel):
@@ -33,8 +33,12 @@ class Session(ITDBaseModel):
33
33
  client_name: str = Field(alias='clientName') # cant use "client"
34
34
  client_version: str = Field(alias='clientVersion')
35
35
 
36
+ def revoke(self):
37
+ revoke(self.client, self.id)
38
+ del self
39
+
36
40
  def delete(self):
37
- delete_session(self.client, self.id)
41
+ self.revoke()
38
42
 
39
43
  def __init__(self, session: dict, client: Client | None = None) -> None:
40
44
  super().__init__(client)
@@ -72,6 +76,14 @@ class Sessions(ITDBaseModel, list[Session]):
72
76
  self.extend([Session(session) for session in get_sessions(self.client).json()['sessions']])
73
77
  return self
74
78
 
79
+ def revoke_all(self) -> int:
80
+ revoked = revoke_all(self.client).json()['revokedCount']
81
+ self.clear()
82
+ return revoked
83
+
84
+ def delete_all(self) -> int:
85
+ return self.revoke_all()
86
+
75
87
  @classmethod
76
88
  def empty(cls):
77
89
  instance = cls.__new__(cls)
@@ -1,12 +1,12 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING
2
+ from typing import TYPE_CHECKING, override
3
3
  from uuid import UUID
4
4
  from datetime import datetime
5
5
 
6
6
  from pydantic import Field, BaseModel, field_validator
7
7
 
8
- from itd.base import ITDBaseModel, refresh_wrapper, ITDList
9
- from itd.enums import AccessType, Unset, Role, ReportReason, ReportTargetType
8
+ from itd.base import ITDBaseModel, ITDList
9
+ from itd.enums import AccessType, Unset, Role, ReportReason, ReportTargetType, LoadStatus
10
10
  from itd.pin import Pin
11
11
  from itd.poll import NewPoll
12
12
  from itd.report import Report
@@ -53,9 +53,8 @@ class Profile(ITDBaseModel):
53
53
  user_id: UUID | None = Field(None, alias='userId')
54
54
  roles: list[Role] | None = None
55
55
 
56
- @refresh_wrapper
57
- def refresh(self, client: Client | None = None):
58
- return get_profile(client or self.client).json()
56
+ def _refresh(self, *, client: Client):
57
+ return get_profile(client).json()
59
58
 
60
59
  def __str__(self) -> str:
61
60
  if self.user:
@@ -77,9 +76,8 @@ class Privacy(ITDBaseModel):
77
76
  likes_visibility: AccessType = Field(alias='likesVisibility')
78
77
  show_last_seen: bool = Field(alias='showLastSeen')
79
78
 
80
- @refresh_wrapper
81
- def refresh(self, client: Client | None = None):
82
- return get_privacy(client or self.client).json()
79
+ def _refresh(self, *, client: Client):
80
+ return get_privacy(client).json()
83
81
 
84
82
  def update(self,
85
83
  is_private: bool | None = None,
@@ -138,9 +136,8 @@ class Subscription(ITDBaseModel):
138
136
  for name, value in validated.__dict__.items():
139
137
  setattr(self, name, value)
140
138
 
141
- @refresh_wrapper
142
- def refresh(self, client: Client | None = None): # refreshes only is_active
143
- return get_subscription(client or self.client).json()
139
+ def _refresh(self, *, client: Client): # refreshes only is_active
140
+ return get_subscription(client).json()
144
141
 
145
142
  def pay(self):
146
143
  return pay_subscription(self.client).json()['confirmationUrl']
@@ -195,9 +192,8 @@ class _UserBase(ITDBaseModel):
195
192
  def __str__(self) -> str:
196
193
  return self.display_name
197
194
 
198
- @refresh_wrapper
199
- def refresh(self, client: Client | None = None):
200
- return get_user(client or self.client, self._identifier).json()
195
+ def _refresh(self, *, client: Client):
196
+ return get_user(client, self._identifier).json()
201
197
 
202
198
  @property
203
199
  def posts(self) -> UserPosts:
@@ -254,14 +250,14 @@ class User(_UserBase):
254
250
  created_at: datetime | None = Field(None, alias='createdAt') # none if blocked
255
251
 
256
252
  @classmethod
257
- def _from_dict(cls, data: dict, set_loaded: bool = True, client: Client | None = None):
253
+ def _from_dict(cls, data: dict, client: Client | None = None):
258
254
  instance = cls(data['username'], client)
259
255
  validated = _UserValidate.model_validate(data)
260
256
  instance._fields_from_data = validated.model_fields_set
261
257
  for name, value in validated.__dict__.items():
262
258
  setattr(instance, name, value)
263
259
 
264
- instance._loaded = set_loaded
260
+ instance.load_status = LoadStatus.PARTIALLY
265
261
  return instance
266
262
 
267
263
  @classmethod
@@ -385,13 +381,13 @@ class User(_UserBase):
385
381
  @property
386
382
  def following(self) -> list[User]:
387
383
  if not self._following:
388
- self._following = [User._from_dict(user, False, self.client) for user in get_following(self.client, self._identifier).json()['data']['users']]
384
+ self._following = [User._from_dict(user, client=self.client) for user in get_following(self.client, self._identifier).json()['data']['users']]
389
385
  return self._following
390
386
 
391
387
  @property
392
388
  def followers(self) -> list[User]:
393
389
  if not self._followers:
394
- self._followers = [User._from_dict(user, False, self.client) for user in get_followers(self.client, self._identifier).json()['data']['users']]
390
+ self._followers = [User._from_dict(user, client=self.client) for user in get_followers(self.client, self._identifier).json()['data']['users']]
395
391
  return self._followers
396
392
 
397
393
 
@@ -439,7 +435,7 @@ class Me(_UserBase):
439
435
  if hasattr(self, name):
440
436
  setattr(instance, name, getattr(self, name))
441
437
  instance._identifier = self.id
442
- instance._loaded = False
438
+ instance.load_status = LoadStatus.PARTIALLY
443
439
  return instance
444
440
 
445
441
  def delete(self) -> datetime: # should not use other client, because you can get Me only from current client
@@ -531,9 +527,8 @@ class Me(_UserBase):
531
527
  self._pins = [Pin(pin, self) for pin in get_pins(self.client).json()['data']['pins']]
532
528
  return self._pins
533
529
 
534
- @refresh_wrapper
535
- def refresh(self, client: Client | None = None):
536
- user = get_user(client or self.client, self._identifier).json()
530
+ def _refresh(self, *, client: Client):
531
+ user = get_user(client, self._identifier).json()
537
532
  if user.get('isDeleted'):
538
533
  exc = AccountDeletedError()
539
534
  exc.can_restore = user.get('canRestore', True)
@@ -590,7 +585,7 @@ class Followers(ITDList[User]):
590
585
  return data['pagination']['page'] + 1
591
586
 
592
587
  def _extend(self, objects: list, client: Client):
593
- self.extend([User._from_dict(user, False, client) for user in objects])
588
+ self.extend([User._from_dict(user, client=client) for user in objects])
594
589
 
595
590
  def __setattr__(self, name: str, value) -> None:
596
591
  if name == '_client':
@@ -620,6 +615,6 @@ class WhoToFollow(ITDBaseModel, list[User]):
620
615
  super().__init__(client)
621
616
  self.refresh()
622
617
 
623
- def refresh(self):
618
+ def refresh(self, *, client: Client | None = None):
624
619
  self.clear()
625
- self.extend([User._from_dict(user, False, self.client) for user in get_who_to_follow(self.client).json()['users']])
620
+ self.extend([User.from_dict(user, client=client or self.client) for user in get_who_to_follow(self.client).json()['users']])
@@ -1,58 +1,58 @@
1
- [build-system]
2
- requires = ["uv_build~=0.11"]
3
- build-backend = "uv_build"
4
-
5
- [tool.uv.build-backend]
6
- module-name = 'itd'
7
- module-root = ''
8
-
9
- [tool.commitizen]
10
- name = "cz_conventional_commits"
11
- tag_format = "v$version"
12
- version_scheme = "semver2"
13
- version_provider = "uv"
14
-
15
- [project]
16
- name = "itd-sdk"
17
- version = "2.4.0"
18
- description = "ITD client for python"
19
- readme = "README.md"
20
- authors = [
21
- { name = "firedotguy", email = "nta16022013@gmail.com" }
22
- ]
23
- classifiers = [
24
- 'Development Status :: 5 - Production/Stable',
25
- 'Natural Language :: Russian',
26
- 'Programming Language :: Python',
27
- 'Programming Language :: Python :: 3',
28
- 'Framework :: Pydantic :: 2'
29
- ]
30
- keywords = ['itd', 'itdsocial']
31
- license = "MIT"
32
- dependencies = [
33
- "requests~=2.32.5",
34
- "pydantic>=2.12.5,<2.14.0",
35
- "sseclient-py~=1.9.0",
36
- ]
37
- requires-python = ">=3.9"
38
-
39
- [project.urls]
40
- Homepage = 'https://itdsdk.qzz.io'
41
- Documentation = 'https://itdsdk.qzz.io/docs'
42
-
43
- [project.optional-dependencies]
44
- filetype = [
45
- "filetype>=1.2.0",
46
- ]
47
- logs = [
48
- "rich>=15.0.0",
49
- ]
50
- date = [
51
- "dateparser>=1.2.2",
52
- ]
53
-
54
- [dependency-groups]
55
- dev = [
56
- "pytest>=8.4.2",
57
- "python-dotenv>=1.2.1",
58
- ]
1
+ [build-system]
2
+ requires = ["uv_build~=0.11"]
3
+ build-backend = "uv_build"
4
+
5
+ [tool.uv.build-backend]
6
+ module-name = 'itd'
7
+ module-root = ''
8
+
9
+ [tool.commitizen]
10
+ name = "cz_conventional_commits"
11
+ tag_format = "v$version"
12
+ version_scheme = "semver2"
13
+ version_provider = "uv"
14
+
15
+ [project]
16
+ name = "itd-sdk"
17
+ version = "2.4.1"
18
+ description = "ITD client for python"
19
+ readme = "README.md"
20
+ authors = [
21
+ { name = "firedotguy", email = "nta16022013@gmail.com" }
22
+ ]
23
+ classifiers = [
24
+ 'Development Status :: 5 - Production/Stable',
25
+ 'Natural Language :: Russian',
26
+ 'Programming Language :: Python',
27
+ 'Programming Language :: Python :: 3',
28
+ 'Framework :: Pydantic :: 2'
29
+ ]
30
+ keywords = ['itd', 'itdsocial']
31
+ license = "MIT"
32
+ dependencies = [
33
+ "requests~=2.32.5",
34
+ "pydantic>=2.12.5,<2.14.0",
35
+ "sseclient-py~=1.9.0",
36
+ ]
37
+ requires-python = ">=3.9"
38
+
39
+ [project.urls]
40
+ Homepage = 'https://itdsdk.qzz.io'
41
+ Documentation = 'https://itdsdk.qzz.io/docs'
42
+
43
+ [project.optional-dependencies]
44
+ filetype = [
45
+ "filetype>=1.2.0",
46
+ ]
47
+ logs = [
48
+ "rich>=15.0.0",
49
+ ]
50
+ date = [
51
+ "dateparser>=1.2.2",
52
+ ]
53
+
54
+ [dependency-groups]
55
+ dev = [
56
+ "pytest>=8.4.2",
57
+ "python-dotenv>=1.2.1",
58
+ ]
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