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.
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/PKG-INFO +1 -1
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/sessions.py +2 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/base.py +35 -36
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/clan.py +2 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/client.py +2 -1
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/comment.py +2 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/enums.py +6 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/file.py +20 -20
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/hashtag.py +3 -4
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/notification.py +2 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/post.py +21 -29
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/report.py +2 -4
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/session.py +14 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/user.py +21 -26
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/pyproject.toml +58 -58
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/README.md +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/__init__.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/_default.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/__init__.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/auth.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/comments.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/dwell.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/etc.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/files.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/hashtags.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/notifications.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/pins.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/platform.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/polls.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/posts.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/reports.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/search.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/subscription.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/users.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/api/verification.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/exceptions.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/logger.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/pin.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/poll.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/request.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/span.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/utils.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.1}/itd/version.py +0 -0
|
@@ -14,10 +14,10 @@ def get_sessions(client: Client):
|
|
|
14
14
|
|
|
15
15
|
@catch_errors()
|
|
16
16
|
@rate_limit()
|
|
17
|
-
def
|
|
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
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
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 (
|
|
92
|
-
l.info('refresh %s
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
58
|
-
|
|
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=
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
318
|
+
instance.load_status = LoadStatus.PARTIALLY
|
|
317
319
|
instance._post_refresh()
|
|
318
320
|
|
|
319
321
|
return instance
|
|
320
322
|
|
|
321
323
|
@classmethod
|
|
322
|
-
def
|
|
323
|
-
instance =
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
-
|
|
535
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|