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