itd-sdk 2.4.0__tar.gz → 2.4.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 (43) hide show
  1. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/PKG-INFO +1 -1
  2. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/sessions.py +2 -2
  3. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/base.py +38 -37
  4. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/clan.py +2 -2
  5. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/client.py +3 -1
  6. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/comment.py +2 -2
  7. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/enums.py +6 -2
  8. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/file.py +20 -22
  9. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/hashtag.py +3 -4
  10. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/notification.py +2 -2
  11. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/post.py +21 -29
  12. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/report.py +2 -7
  13. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/session.py +14 -2
  14. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/user.py +25 -33
  15. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/pyproject.toml +1 -1
  16. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/README.md +0 -0
  17. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/__init__.py +0 -0
  18. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/_default.py +0 -0
  19. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/__init__.py +0 -0
  20. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/auth.py +0 -0
  21. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/comments.py +0 -0
  22. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/dwell.py +0 -0
  23. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/etc.py +0 -0
  24. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/files.py +0 -0
  25. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/hashtags.py +0 -0
  26. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/notifications.py +0 -0
  27. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/pins.py +0 -0
  28. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/platform.py +0 -0
  29. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/polls.py +0 -0
  30. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/posts.py +0 -0
  31. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/reports.py +0 -0
  32. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/search.py +0 -0
  33. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/subscription.py +0 -0
  34. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/users.py +0 -0
  35. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/verification.py +0 -0
  36. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/exceptions.py +0 -0
  37. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/logger.py +0 -0
  38. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/pin.py +0 -0
  39. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/poll.py +0 -0
  40. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/request.py +0 -0
  41. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/span.py +0 -0
  42. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/utils.py +0 -0
  43. {itd_sdk-2.4.0 → itd_sdk-2.4.2}/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.2
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')
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
- from typing import Any, Callable, TYPE_CHECKING, Iterator, TypeVar, overload
2
+ from typing import Any, Callable, TYPE_CHECKING, Iterator, TypeVar, overload, Literal
3
3
  from functools import wraps
4
4
  from time import sleep
5
5
  from datetime import datetime, timedelta
@@ -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,41 @@ 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: raise NotImplementedError()
70
+
71
+ def refresh(self, *, client: Client | None = None) -> Any:
72
+ if not self._refreshable:
73
+ l.warning(f'{self.__class__.__name__} is not refreshable but refresh is called')
74
+ self.load_status = LoadStatus.LOADING
75
+ self._fill_from_data(self._refresh(client=client or self.client))
76
+ self.load_status = LoadStatus.FULL
77
+ return self
78
+
79
+
80
+ def _fill_from_data(self, data: dict):
81
+ assert self._validator, 'Unable to use fill_from_data without a validator'
82
+ validated = self._validator().model_validate(data) # pyright: ignore[reportCallIssue]
83
+ self._fields_from_data = validated.model_fields_set
84
+ for name, value in validated.__dict__.items():
85
+ setattr(self, name, value)
86
+
87
+ self._post_refresh()
88
+
89
+
90
+ @classmethod
91
+ def from_dict(cls, data: dict, *, client: Client | None = None):
92
+ instance = cls.__new__(cls)
93
+ ITDBaseModel.__init__(instance, client)
94
+
95
+ instance._fill_from_data(data)
96
+ instance.load_status = LoadStatus.PARTIALLY
97
+ return instance
74
98
 
75
99
 
76
100
  if not TYPE_CHECKING:
@@ -78,20 +102,20 @@ class ITDBaseModel:
78
102
  value = _getattr(self, name)
79
103
  if (
80
104
  name.startswith('_') or # приватный аттрибут
81
- name in ('client', 'model_fields_set') or
105
+ name in ('client', 'model_fields_set', 'load_status') or
82
106
  not _getattr(self, '_refreshable') or # не рефрешабельная модель
83
107
  not _getattr(self, 'client').config.auto_load or # отключено в конфиге
84
108
  callable(value) or # функция
109
+ self.load_status == LoadStatus.LOADING or
85
110
  (not _getattr(self, 'client').config.load_comments_from_post and _is_comments_on_post(value, self)) or
86
111
  (isinstance(value, ITDBaseModel) and not value._load_with_parent)
87
112
  ):
88
113
  # l.debug('return %s as is', name)
89
114
  return object.__getattribute__(self, name)
90
115
 
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)
116
+ if isinstance(value, FieldInfo) or (self.load_status != LoadStatus.FULL and name not in self._fields_from_data):
117
+ l.info('refresh %s field=%s load_status=%s', self.__class__.__name__, name, self.load_status.value)
93
118
  self.refresh()
94
- return object.__getattribute__(self, name)
95
119
 
96
120
  return object.__getattribute__(self, name)
97
121
 
@@ -179,7 +203,7 @@ class ITDList[T](ITDBaseModel, list[T]):
179
203
  def _get_objects(data: dict) -> list[dict]:
180
204
  return []
181
205
 
182
- def refresh(self, count: int | All | Batch | None = None, limit: int | Batch = BATCH, client: Client | None = None) -> list[T]:
206
+ def refresh(self, count: int | All | Batch | None = None, limit: int | Batch = BATCH, *, client: Client | None = None) -> list[T]:
183
207
  """Обновить список (удалить все элементы и загрузить заново)
184
208
 
185
209
  Args:
@@ -265,29 +289,6 @@ class ITDList[T](ITDBaseModel, list[T]):
265
289
  return self.load_all()
266
290
 
267
291
 
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
292
  def _filter_bytes(args: tuple):
292
293
  filtered = []
293
294
  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
@@ -52,6 +52,7 @@ class Config:
52
52
 
53
53
  timeout: float = 30
54
54
  timeout_file: float = 120
55
+ timeout_file_download: float = 60
55
56
 
56
57
  url: str = 'xn--d1ah4a.com'
57
58
  url_api: str | None = None
@@ -75,6 +76,7 @@ class Config:
75
76
  dwell_send_interval: float = 2
76
77
  dwell_save_on_quit: bool = True
77
78
  dwell_wait_durations: bool = True
79
+ post_view_increment: bool = False
78
80
 
79
81
  post_update_stats: bool = False
80
82
  post_update_stats_interval: int = 3
@@ -285,7 +287,7 @@ class Client:
285
287
  tuple[list[User], list[Hashtag]]: Результат поиска
286
288
  """
287
289
  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']]
290
+ return [User._from_dict(user, client=self) for user in res['users']], [Hashtag._from_dict(hashtag, self) for hashtag in res['hashtags']]
289
291
 
290
292
  def search_users(self, query: str, limit: int = 20) -> list[User]:
291
293
  """Поиск пользователей
@@ -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,34 @@ 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._fill_from_data(upload_file(client or self.client, self.filename, self.data).json())
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 delete(self, *, client: Client | None = None) -> None:
55
+ delete_file(client or self.client, self.id)
59
56
 
60
57
  def download(self, name: str | None = None) -> None:
61
58
  with open(name or self.filename, 'wb') as fl:
62
- fl.write(get(self.url, timeout=60).content)
59
+ fl.write(get(self.url, timeout=self.client.config.timeout_file_download).content)
63
60
 
64
61
  def __str__(self) -> str:
65
62
  return self.filename
@@ -73,6 +70,7 @@ class _FileValidate(BaseModel, File):
73
70
  class PostAttach(ITDBaseModel):
74
71
  _validator = lambda _: _PostAttachValidate
75
72
  _post: Post
73
+ _refreshable = False
76
74
 
77
75
  id: UUID
78
76
  type: AttachType = AttachType.IMAGE
@@ -94,7 +92,7 @@ class PostAttach(ITDBaseModel):
94
92
  name (str): Имя файла.
95
93
  """
96
94
  with open(name, 'wb') as fl:
97
- fl.write(get(self.url, timeout=self.client.config.timeout_file).content)
95
+ fl.write(get(self.url, timeout=self.client.config.timeout_file_download).content)
98
96
 
99
97
  def record_open(self, client: Client | None = None):
100
98
  """Записать событие открытия фото
@@ -163,7 +161,7 @@ class CommentAttach(PostAttach):
163
161
  name (str | None, optional): Имя файла. None - имя, под которым был загружен этот файл. Defaults to None.
164
162
  """
165
163
  with open(name or self.filename, 'wb') as fl:
166
- fl.write(get(self.url, timeout=self.client.config.timeout_file).content)
164
+ fl.write(get(self.url, timeout=self.client.config.timeout_file_download).content)
167
165
 
168
166
 
169
167
  class _CommentAttachValidate(BaseModel, CommentAttach):
@@ -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:
@@ -26,12 +26,7 @@ class Report(ITDBaseModel):
26
26
  self.reason = reason
27
27
  self.description = description
28
28
 
29
- self.refresh()
30
-
31
-
32
- @refresh_wrapper
33
- def refresh(self, client: Client | None = None):
34
- return report(client or self.client, self.target_id, self.target_type, self.reason, self.description).json()['data']
29
+ self._fill_from_data(report(client or self.client, self.target_id, self.target_type, self.reason, self.description).json()['data'])
35
30
 
36
31
 
37
32
 
@@ -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,
@@ -125,6 +123,7 @@ class _PrivacyValidate(BaseModel, Privacy):
125
123
  class Subscription(ITDBaseModel):
126
124
  _validator = lambda _: _SubscriptionValidate
127
125
  _payment_methods: list
126
+ _refreshable = False
128
127
 
129
128
  active: bool = Field(False, alias='isActive')
130
129
  expires_at: datetime | None = Field(None, alias='expiresAt')
@@ -138,9 +137,8 @@ class Subscription(ITDBaseModel):
138
137
  for name, value in validated.__dict__.items():
139
138
  setattr(self, name, value)
140
139
 
141
- @refresh_wrapper
142
- def refresh(self, client: Client | None = None): # refreshes only is_active
143
- return get_subscription(client or self.client).json()
140
+ def _refresh(self, *, client: Client): # refreshes only is_active
141
+ return get_subscription(client).json()
144
142
 
145
143
  def pay(self):
146
144
  return pay_subscription(self.client).json()['confirmationUrl']
@@ -195,9 +193,8 @@ class _UserBase(ITDBaseModel):
195
193
  def __str__(self) -> str:
196
194
  return self.display_name
197
195
 
198
- @refresh_wrapper
199
- def refresh(self, client: Client | None = None):
200
- return get_user(client or self.client, self._identifier).json()
196
+ def _refresh(self, *, client: Client):
197
+ return get_user(client, self._identifier).json()
201
198
 
202
199
  @property
203
200
  def posts(self) -> UserPosts:
@@ -254,14 +251,14 @@ class User(_UserBase):
254
251
  created_at: datetime | None = Field(None, alias='createdAt') # none if blocked
255
252
 
256
253
  @classmethod
257
- def _from_dict(cls, data: dict, set_loaded: bool = True, client: Client | None = None):
254
+ def _from_dict(cls, data: dict, client: Client | None = None):
258
255
  instance = cls(data['username'], client)
259
256
  validated = _UserValidate.model_validate(data)
260
257
  instance._fields_from_data = validated.model_fields_set
261
258
  for name, value in validated.__dict__.items():
262
259
  setattr(instance, name, value)
263
260
 
264
- instance._loaded = set_loaded
261
+ instance.load_status = LoadStatus.PARTIALLY
265
262
  return instance
266
263
 
267
264
  @classmethod
@@ -385,13 +382,13 @@ class User(_UserBase):
385
382
  @property
386
383
  def following(self) -> list[User]:
387
384
  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']]
385
+ self._following = [User._from_dict(user, client=self.client) for user in get_following(self.client, self._identifier).json()['data']['users']]
389
386
  return self._following
390
387
 
391
388
  @property
392
389
  def followers(self) -> list[User]:
393
390
  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']]
391
+ self._followers = [User._from_dict(user, client=self.client) for user in get_followers(self.client, self._identifier).json()['data']['users']]
395
392
  return self._followers
396
393
 
397
394
 
@@ -439,7 +436,7 @@ class Me(_UserBase):
439
436
  if hasattr(self, name):
440
437
  setattr(instance, name, getattr(self, name))
441
438
  instance._identifier = self.id
442
- instance._loaded = False
439
+ instance.load_status = LoadStatus.PARTIALLY
443
440
  return instance
444
441
 
445
442
  def delete(self) -> datetime: # should not use other client, because you can get Me only from current client
@@ -531,9 +528,8 @@ class Me(_UserBase):
531
528
  self._pins = [Pin(pin, self) for pin in get_pins(self.client).json()['data']['pins']]
532
529
  return self._pins
533
530
 
534
- @refresh_wrapper
535
- def refresh(self, client: Client | None = None):
536
- user = get_user(client or self.client, self._identifier).json()
531
+ def _refresh(self, *, client: Client):
532
+ user = get_user(client, self._identifier).json()
537
533
  if user.get('isDeleted'):
538
534
  exc = AccountDeletedError()
539
535
  exc.can_restore = user.get('canRestore', True)
@@ -541,13 +537,9 @@ class Me(_UserBase):
541
537
  raise exc
542
538
  return user
543
539
 
544
- if not TYPE_CHECKING:
545
- def __getattribute__(self, name: str):
546
- value = super().__getattribute__(name)
547
- if name == 'pin' and value is not None and getattr(value, '_user', None) is None:
548
- value._user = self
549
- return value
550
-
540
+ def _post_refresh(self):
541
+ if self.pin:
542
+ self.pin._user = self
551
543
 
552
544
 
553
545
  class _MeValidate(BaseModel, Me):
@@ -590,7 +582,7 @@ class Followers(ITDList[User]):
590
582
  return data['pagination']['page'] + 1
591
583
 
592
584
  def _extend(self, objects: list, client: Client):
593
- self.extend([User._from_dict(user, False, client) for user in objects])
585
+ self.extend([User._from_dict(user, client=client) for user in objects])
594
586
 
595
587
  def __setattr__(self, name: str, value) -> None:
596
588
  if name == '_client':
@@ -620,6 +612,6 @@ class WhoToFollow(ITDBaseModel, list[User]):
620
612
  super().__init__(client)
621
613
  self.refresh()
622
614
 
623
- def refresh(self):
615
+ def refresh(self, *, client: Client | None = None):
624
616
  self.clear()
625
- self.extend([User._from_dict(user, False, self.client) for user in get_who_to_follow(self.client).json()['users']])
617
+ self.extend([User.from_dict(user, client=client or self.client) for user in get_who_to_follow(self.client).json()['users']])
@@ -14,7 +14,7 @@ version_provider = "uv"
14
14
 
15
15
  [project]
16
16
  name = "itd-sdk"
17
- version = "2.4.0"
17
+ version = "2.4.2"
18
18
  description = "ITD client for python"
19
19
  readme = "README.md"
20
20
  authors = [
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