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.
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/PKG-INFO +1 -1
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/sessions.py +2 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/base.py +38 -37
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/clan.py +2 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/client.py +3 -1
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/comment.py +2 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/enums.py +6 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/file.py +20 -22
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/hashtag.py +3 -4
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/notification.py +2 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/post.py +21 -29
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/report.py +2 -7
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/session.py +14 -2
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/user.py +25 -33
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/pyproject.toml +1 -1
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/README.md +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/__init__.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/_default.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/__init__.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/auth.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/comments.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/dwell.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/etc.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/files.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/hashtags.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/notifications.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/pins.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/platform.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/polls.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/posts.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/reports.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/search.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/subscription.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/users.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/api/verification.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/exceptions.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/logger.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/pin.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/poll.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/request.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/span.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/itd/utils.py +0 -0
- {itd_sdk-2.4.0 → itd_sdk-2.4.2}/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')
|
|
@@ -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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
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 (
|
|
92
|
-
l.info('refresh %s
|
|
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.
|
|
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,
|
|
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
|
|
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,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.
|
|
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
|
-
|
|
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 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=
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
@@ -26,12 +26,7 @@ class Report(ITDBaseModel):
|
|
|
26
26
|
self.reason = reason
|
|
27
27
|
self.description = description
|
|
28
28
|
|
|
29
|
-
self.
|
|
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,
|
|
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,
|
|
@@ -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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
-
|
|
535
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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,
|
|
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.
|
|
617
|
+
self.extend([User.from_dict(user, client=client or self.client) for user in get_who_to_follow(self.client).json()['users']])
|
|
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
|