itd-sdk 2.1.1__tar.gz → 2.2.0__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.1.1 → itd_sdk-2.2.0}/PKG-INFO +1 -1
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/auth.py +5 -4
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/hashtags.py +3 -2
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/search.py +2 -1
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/base.py +8 -5
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/client.py +33 -9
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/enums.py +25 -5
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/exceptions.py +7 -3
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/post.py +30 -13
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/user.py +11 -9
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/pyproject.toml +1 -1
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/README.md +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/__init__.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/_default.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/__init__.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/comments.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/etc.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/files.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/notifications.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/pins.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/polls.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/posts.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/reports.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/subscription.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/users.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/verification.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/clan.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/comment.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/file.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/hashtag.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/logger.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/notification.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/pin.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/poll.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/report.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/request.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/span.py +0 -0
- {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/utils.py +0 -0
|
@@ -3,15 +3,16 @@ from typing import TYPE_CHECKING
|
|
|
3
3
|
|
|
4
4
|
from requests import Response
|
|
5
5
|
|
|
6
|
-
from itd.base import catch_errors
|
|
7
|
-
from itd.
|
|
6
|
+
from itd.base import catch_errors, rate_limit
|
|
7
|
+
from itd.enums import AuthLevel
|
|
8
8
|
from itd.exceptions import InvalidPasswordError, SamePasswordError, InvalidOldPasswordError, SessionNotFoundError, SessionExpiredError, SessionRevokedError
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from itd.client import Client
|
|
11
11
|
|
|
12
|
+
@rate_limit()
|
|
12
13
|
@catch_errors(SessionExpiredError(), SessionNotFoundError(), SessionRevokedError())
|
|
13
14
|
def refresh_token(client: Client) -> Response:
|
|
14
|
-
return
|
|
15
|
+
return client.request('post', 'v1/auth/refresh', level=AuthLevel.REFRESH)
|
|
15
16
|
|
|
16
17
|
@catch_errors(InvalidPasswordError(), SamePasswordError(), InvalidOldPasswordError())
|
|
17
18
|
def change_password(client: Client, old: str, new: str) -> Response:
|
|
@@ -19,4 +20,4 @@ def change_password(client: Client, old: str, new: str) -> Response:
|
|
|
19
20
|
|
|
20
21
|
@catch_errors()
|
|
21
22
|
def logout(client: Client) -> Response:
|
|
22
|
-
return client.request('post', 'v1/auth/logout')
|
|
23
|
+
return client.request('post', 'v1/auth/logout', level=AuthLevel.REFRESH)
|
|
@@ -4,15 +4,16 @@ from typing import TYPE_CHECKING
|
|
|
4
4
|
|
|
5
5
|
from itd.exceptions import TooLargeError, NotFoundError
|
|
6
6
|
from itd.base import catch_errors, rate_limit
|
|
7
|
+
from itd.enums import AuthLevel
|
|
7
8
|
if TYPE_CHECKING:
|
|
8
9
|
from itd.client import Client
|
|
9
10
|
|
|
10
11
|
@rate_limit()
|
|
11
12
|
@catch_errors()
|
|
12
13
|
def get_hashtags(client: Client, limit: int = 10):
|
|
13
|
-
return client.request('get', 'hashtags/trending', {'limit': limit})
|
|
14
|
+
return client.request('get', 'hashtags/trending', {'limit': limit}, level=AuthLevel.NO)
|
|
14
15
|
|
|
15
16
|
@rate_limit()
|
|
16
17
|
@catch_errors(TooLargeError('Hashtag'), NotFoundError('Hashtag', json_check=lambda json: json.get('data', {}).get('hashtag', '') is None))
|
|
17
18
|
def get_posts_by_hashtag(client: Client, hashtag: str, cursor: UUID | None = None, limit: int = 20):
|
|
18
|
-
return client.request('get', f'hashtags/{hashtag}/posts', {'limit': limit, 'cursor': cursor})
|
|
19
|
+
return client.request('get', f'hashtags/{hashtag}/posts', {'limit': limit, 'cursor': cursor}, level=AuthLevel.NO)
|
|
@@ -4,9 +4,10 @@ from typing import TYPE_CHECKING
|
|
|
4
4
|
if TYPE_CHECKING:
|
|
5
5
|
from itd.client import Client
|
|
6
6
|
from itd.exceptions import ValidationError
|
|
7
|
+
from itd.enums import AuthLevel
|
|
7
8
|
from itd.base import catch_errors, rate_limit
|
|
8
9
|
|
|
9
10
|
@rate_limit(None, 0.5, 3)
|
|
10
11
|
@catch_errors(ValidationError())
|
|
11
12
|
def search(client: Client, query: str, user_limit: int = 5, hashtag_limit: int = 5):
|
|
12
|
-
return client.request('get', 'search', {'userLimit': user_limit, 'hashtagLimit': hashtag_limit, 'q': query})
|
|
13
|
+
return client.request('get', 'search', {'userLimit': user_limit, 'hashtagLimit': hashtag_limit, 'q': query}, level=AuthLevel.NO)
|
|
@@ -12,7 +12,7 @@ from pydantic_core import PydanticUndefinedType
|
|
|
12
12
|
|
|
13
13
|
from itd._default import get_default_client
|
|
14
14
|
from itd.logger import get_logger
|
|
15
|
-
from itd.exceptions import ITDException, ValidationError, RateLimitError, UnauthorizedError, AccessTokenExpiredError, DEFAULT_ERRORS
|
|
15
|
+
from itd.exceptions import ITDException, ValidationError, RateLimitError, UnauthorizedError, AccessTokenExpiredError, DEFAULT_ERRORS, InternalError
|
|
16
16
|
from itd.enums import All, ALL, DebugResponseMode, RateLimitMode, BATCH, Batch
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
18
|
from itd.client import Client
|
|
@@ -147,7 +147,6 @@ class ITDList[T](ITDBaseModel, list[T]):
|
|
|
147
147
|
length = len(objects)
|
|
148
148
|
if objects and (client or self.client).config.userposts_add_pinned_post and length == batch + 1: # skip pinned post
|
|
149
149
|
length -= 1
|
|
150
|
-
l.debug('%s', objects)
|
|
151
150
|
|
|
152
151
|
if left is not None:
|
|
153
152
|
left -= length
|
|
@@ -370,12 +369,16 @@ def rate_limit(delay_min: float | None = None, delay_mid: float | None = None, d
|
|
|
370
369
|
sleep(max(delay, 0))
|
|
371
370
|
client.last_actions[func.__name__] = datetime.now()
|
|
372
371
|
|
|
372
|
+
if not client.config.retry_enabled:
|
|
373
|
+
return func(client, *args, **kwargs)
|
|
374
|
+
|
|
373
375
|
while True:
|
|
374
376
|
try:
|
|
375
377
|
return func(client, *args, **kwargs)
|
|
376
|
-
except
|
|
377
|
-
|
|
378
|
-
|
|
378
|
+
except (client.config._retry_exceptions) as e:
|
|
379
|
+
retry_after = getattr(e, 'retry_after', client.config.rate_limit_wait) or client.config.retry_delay
|
|
380
|
+
l.info('%s on %s: wait %ss', e.__class__.__name__, func.__name__, retry_after)
|
|
381
|
+
sleep(retry_after)
|
|
379
382
|
|
|
380
383
|
return wrapper
|
|
381
384
|
return decorator
|
|
@@ -6,12 +6,13 @@ from dataclasses import dataclass, field
|
|
|
6
6
|
from requests import Session
|
|
7
7
|
from requests.utils import default_user_agent
|
|
8
8
|
from requests.adapters import HTTPAdapter
|
|
9
|
+
from requests.exceptions import RequestException
|
|
9
10
|
|
|
10
11
|
from itd._default import _default_client, set_default_client
|
|
11
|
-
from itd.exceptions import UnauthorizedError, InsufficientAuthLevelError, AccessTokenExpiredError
|
|
12
|
+
from itd.exceptions import UnauthorizedError, InsufficientAuthLevelError, AccessTokenExpiredError, RateLimitError, InternalError
|
|
12
13
|
from itd.hashtag import Hashtag
|
|
13
14
|
from itd.request import fetch, decode_jwt_payload
|
|
14
|
-
from itd.enums import RateLimitMode, All, DebugResponseMode, ParseMode, Batch, BATCH, UserAgent
|
|
15
|
+
from itd.enums import RateLimitMode, All, DebugResponseMode, ParseMode, Batch, BATCH, UserAgent, AuthLevel
|
|
15
16
|
from itd.user import Me, User
|
|
16
17
|
from itd.api.auth import refresh_token, change_password, logout
|
|
17
18
|
from itd.api.search import search
|
|
@@ -45,6 +46,13 @@ class Config:
|
|
|
45
46
|
solve_challenge: bool = True
|
|
46
47
|
load_comments_from_post: bool = False
|
|
47
48
|
parse_mode: ParseMode = ParseMode.NO
|
|
49
|
+
rate_limit_wait: int | None = None # DEPRECATED
|
|
50
|
+
retry_on_rate_limits: bool | None = None # DEPRECATED
|
|
51
|
+
retry_enabled: bool = True
|
|
52
|
+
retry_delay: float = 10 # delay before next attempt (after rate limit error) if retry_after is not provided in request
|
|
53
|
+
retry_max_retries: int | None = None # none for no limit
|
|
54
|
+
retry_exceptions: tuple[type[Exception]] | list[type[Exception]] | None = None
|
|
55
|
+
bypass_auth_level: bool = False
|
|
48
56
|
|
|
49
57
|
def __post_init__(self):
|
|
50
58
|
if self.rate_limit_default:
|
|
@@ -71,10 +79,19 @@ class Config:
|
|
|
71
79
|
case _:
|
|
72
80
|
self._user_agent = self.user_agent
|
|
73
81
|
|
|
82
|
+
if self.rate_limit_wait is not None:
|
|
83
|
+
l.warning('config.rate_limit_wait is deprecated and will be removed in 2.4.0. Please use config.retry_delay')
|
|
84
|
+
self.retry_delay = self.rate_limit_wait
|
|
85
|
+
if self.retry_on_rate_limits is not None:
|
|
86
|
+
l.warning('config.retry_on_rate_limits is deprecated and will be removed in 2.4.0. Please use config.retry_enabled')
|
|
87
|
+
self.retry_enabled = self.retry_on_rate_limits
|
|
88
|
+
|
|
89
|
+
self._retry_exceptions = (tuple(self.retry_exceptions) if isinstance(self.retry_exceptions, list) else self.retry_exceptions) or (RateLimitError, InternalError, RequestException)
|
|
74
90
|
|
|
75
91
|
|
|
76
92
|
|
|
77
93
|
class Client:
|
|
94
|
+
auth_level: AuthLevel = AuthLevel.NO
|
|
78
95
|
access_token: str | None = None
|
|
79
96
|
refresh_token: str | None = None
|
|
80
97
|
_user = None
|
|
@@ -90,18 +107,20 @@ class Client:
|
|
|
90
107
|
self.session.mount('https://', adapter)
|
|
91
108
|
|
|
92
109
|
if access:
|
|
110
|
+
self.auth_level = AuthLevel.ACCESS
|
|
93
111
|
self.access_token = access.replace('Bearer ', '')
|
|
94
112
|
|
|
95
113
|
if refresh:
|
|
114
|
+
self.auth_level = AuthLevel.REFRESH
|
|
96
115
|
self.refresh_token = refresh
|
|
97
116
|
self.session.cookies.set('refresh_token', refresh, path='/', domain=self.config.url)
|
|
98
|
-
if access is None:
|
|
99
|
-
|
|
117
|
+
# if access is None:
|
|
118
|
+
# self.refresh_auth()
|
|
100
119
|
|
|
101
120
|
if _default_client is None or config.is_default:
|
|
102
121
|
set_default_client(self)
|
|
103
122
|
|
|
104
|
-
def request(self, method: str, url: str, params: dict = {}, files: dict[str, tuple[str, BufferedReader | bytes]] = {}):
|
|
123
|
+
def request(self, method: str, url: str, params: dict = {}, files: dict[str, tuple[str, BufferedReader | bytes]] = {}, level=AuthLevel.ACCESS):
|
|
105
124
|
"""Сделать запрос
|
|
106
125
|
|
|
107
126
|
Args:
|
|
@@ -110,11 +129,18 @@ class Client:
|
|
|
110
129
|
params (dict, optional): Параметры. Defaults to {}.
|
|
111
130
|
files (dict[str, tuple[str, BufferedReader | bytes]], optional): Файлы. Defaults to {}.
|
|
112
131
|
"""
|
|
113
|
-
l.debug('%s %s params=%s', method.upper(), url, params)
|
|
132
|
+
l.debug('%s %s params=%s authlevel=%s', method.upper(), url, params, level.value)
|
|
133
|
+
|
|
134
|
+
if level > self.auth_level and not self.config.bypass_auth_level:
|
|
135
|
+
raise InsufficientAuthLevelError(self.auth_level, level)
|
|
136
|
+
|
|
137
|
+
if level >= AuthLevel.ACCESS and self.access_token is None and url != 'v1/auth/refresh':
|
|
138
|
+
self.refresh_auth()
|
|
139
|
+
|
|
114
140
|
def _fetch():
|
|
115
141
|
return fetch(self, method, url, params, files)
|
|
116
142
|
|
|
117
|
-
if not self.refresh_token:
|
|
143
|
+
if not self.refresh_token or url == 'v1/auth/refresh':
|
|
118
144
|
return _fetch()
|
|
119
145
|
|
|
120
146
|
try:
|
|
@@ -130,8 +156,6 @@ class Client:
|
|
|
130
156
|
str: Токен
|
|
131
157
|
"""
|
|
132
158
|
l.debug('refresh token')
|
|
133
|
-
if not self.refresh_token:
|
|
134
|
-
raise InsufficientAuthLevelError()
|
|
135
159
|
|
|
136
160
|
res = refresh_token(self)
|
|
137
161
|
res.raise_for_status()
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
|
+
class AuthLevel(Enum):
|
|
5
|
+
NO = 'no'
|
|
6
|
+
ACCESS = 'access'
|
|
7
|
+
REFRESH = 'refresh'
|
|
8
|
+
# LOGIN = 'login' # 🤫
|
|
9
|
+
|
|
10
|
+
def __gt__(self, other):
|
|
11
|
+
return _AUTH_LEVEL_HIERARCHY.index(self) > _AUTH_LEVEL_HIERARCHY.index(other)
|
|
12
|
+
|
|
13
|
+
def __lt__(self, other):
|
|
14
|
+
return _AUTH_LEVEL_HIERARCHY.index(self) < _AUTH_LEVEL_HIERARCHY.index(other)
|
|
15
|
+
|
|
16
|
+
def __ge__(self, other):
|
|
17
|
+
return _AUTH_LEVEL_HIERARCHY.index(self) >= _AUTH_LEVEL_HIERARCHY.index(other)
|
|
18
|
+
|
|
19
|
+
def __le__(self, other):
|
|
20
|
+
return _AUTH_LEVEL_HIERARCHY.index(self) <= _AUTH_LEVEL_HIERARCHY.index(other)
|
|
21
|
+
|
|
22
|
+
_AUTH_LEVEL_HIERARCHY = [AuthLevel.NO, AuthLevel.ACCESS, AuthLevel.REFRESH]
|
|
23
|
+
|
|
4
24
|
class RateLimitMode(Enum):
|
|
5
25
|
NO = 'no'
|
|
6
26
|
MIN = 'min' # for one-time actions (eg script just to like post)
|
|
@@ -81,18 +101,18 @@ class AccessType(Enum):
|
|
|
81
101
|
EVERYONE = 'everyone' # все
|
|
82
102
|
|
|
83
103
|
def __gt__(self, other):
|
|
84
|
-
return
|
|
104
|
+
return _ACCESS_TYPE_HIERARCHY.index(self) > _ACCESS_TYPE_HIERARCHY.index(other)
|
|
85
105
|
|
|
86
106
|
def __lt__(self, other):
|
|
87
|
-
return
|
|
107
|
+
return _ACCESS_TYPE_HIERARCHY.index(self) < _ACCESS_TYPE_HIERARCHY.index(other)
|
|
88
108
|
|
|
89
109
|
def __ge__(self, other):
|
|
90
|
-
return
|
|
110
|
+
return _ACCESS_TYPE_HIERARCHY.index(self) >= _ACCESS_TYPE_HIERARCHY.index(other)
|
|
91
111
|
|
|
92
112
|
def __le__(self, other):
|
|
93
|
-
return
|
|
113
|
+
return _ACCESS_TYPE_HIERARCHY.index(self) <= _ACCESS_TYPE_HIERARCHY.index(other)
|
|
94
114
|
|
|
95
|
-
|
|
115
|
+
_ACCESS_TYPE_HIERARCHY = [AccessType.EVERYONE, AccessType.FOLLOWERS, AccessType.MUTUAL, AccessType.EVERYONE] # 100% that hierarcy is spelled wrong
|
|
96
116
|
|
|
97
117
|
|
|
98
118
|
class SpanType(Enum):
|
|
@@ -2,6 +2,9 @@ from typing import Callable
|
|
|
2
2
|
|
|
3
3
|
from requests import Response
|
|
4
4
|
|
|
5
|
+
from itd.enums import AuthLevel
|
|
6
|
+
|
|
7
|
+
|
|
5
8
|
class ITDException(Exception):
|
|
6
9
|
code: str | None = None # ['error']['code']
|
|
7
10
|
message: str | None = None # ['error']['message']
|
|
@@ -110,8 +113,8 @@ class InvalidPasswordError(PasswordError, ValidateError):
|
|
|
110
113
|
class NoRightsError(ITDException): pass
|
|
111
114
|
|
|
112
115
|
class InsufficientAuthLevelError(NoRightsError):
|
|
113
|
-
def __init__(self) :
|
|
114
|
-
self.text = 'Insufficient auth level'
|
|
116
|
+
def __init__(self, current: AuthLevel, requires: AuthLevel) :
|
|
117
|
+
self.text = f'Insufficient auth level (current is {current.name}, requires {requires.name})'
|
|
115
118
|
|
|
116
119
|
class PinNotOwnedError(NoRightsError):
|
|
117
120
|
code = "PIN_NOT_OWNED"
|
|
@@ -231,6 +234,7 @@ class NotPinnedError(ITDException):
|
|
|
231
234
|
text = 'Post not found or is not pinned'
|
|
232
235
|
|
|
233
236
|
class InternalError(ITDException):
|
|
237
|
+
status_code = 502
|
|
234
238
|
code = 'INTERNAL_ERROR'
|
|
235
239
|
text = 'Internal server error'
|
|
236
240
|
|
|
@@ -252,4 +256,4 @@ class ProfileRequiredError(ITDException):
|
|
|
252
256
|
text = 'No profile. Please create your profile first'
|
|
253
257
|
|
|
254
258
|
|
|
255
|
-
DEFAULT_ERRORS = (RateLimitError(), InvalidAccessTokenError(), UnauthorizedError(), AccessTokenExpiredError(), AccountBannedError(), InternalError(), ProfileRequiredError())
|
|
259
|
+
DEFAULT_ERRORS = (RateLimitError(), InvalidAccessTokenError(), UnauthorizedError(), AccessTokenExpiredError(), AccountBannedError(), InternalError(), ProfileRequiredError(), RefreshTokenMissingError())
|
|
@@ -1,31 +1,41 @@
|
|
|
1
1
|
from uuid import UUID
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
from typing import Literal, overload
|
|
3
|
+
from typing import Literal, overload, TYPE_CHECKING
|
|
4
4
|
from time import sleep
|
|
5
5
|
|
|
6
6
|
from pydantic import Field, BaseModel, field_validator
|
|
7
|
-
from pydantic.fields import FieldInfo
|
|
8
7
|
|
|
9
|
-
from itd.base import ITDBaseModel, refresh_wrapper, ITDList
|
|
10
|
-
from itd.client import Client
|
|
11
8
|
from itd.comment import Comment, Comments
|
|
12
|
-
from itd.enums import PostsTab, UserPostSorting, ReportReason, ReportTargetType, ParseMode, ALL
|
|
13
9
|
from itd.file import PostAttach
|
|
14
10
|
from itd.hashtag import Hashtag
|
|
15
11
|
from itd.poll import Poll, NewPoll, PollOption
|
|
16
12
|
from itd.report import Report
|
|
17
13
|
from itd.span import Span
|
|
18
14
|
from itd.user import User, _UserBase, Me
|
|
19
|
-
|
|
15
|
+
|
|
20
16
|
from itd.api.posts import (
|
|
21
17
|
get_post, create_post, like_post, unlike_post, repost, view_post, pin_post, unpin_post,
|
|
22
18
|
delete_post, restore_post, edit_post, get_posts, get_user_posts, get_liked_posts
|
|
23
19
|
)
|
|
24
20
|
from itd.api.hashtags import get_posts_by_hashtag
|
|
21
|
+
from itd.base import ITDBaseModel, refresh_wrapper, ITDList
|
|
22
|
+
from itd.client import Client
|
|
23
|
+
from itd.enums import PostsTab, UserPostSorting, ReportReason, ReportTargetType, ParseMode, ALL
|
|
25
24
|
from itd.logger import get_logger
|
|
25
|
+
from itd.utils import to_uuid, parse_datetime, format_attachments, ATTACHMENTS, parse_html, parse_md
|
|
26
26
|
|
|
27
27
|
l = get_logger('post')
|
|
28
28
|
|
|
29
|
+
# class ViewerSession:
|
|
30
|
+
# def __init__(self, payload: dict) -> None:
|
|
31
|
+
# self.p = self.post_id = UUID(payload['p'])
|
|
32
|
+
# self.u = self.user_id = UUID(payload['u'])
|
|
33
|
+
# self.e = self.expires_at = datetime.fromtimestamp(payload['e'])
|
|
34
|
+
# if isinstance(payload['s'], list):
|
|
35
|
+
# self.s = self.source = [ViewSource(source) for source in payload['s']]
|
|
36
|
+
# else:
|
|
37
|
+
# self.s = self.source = ViewSource(payload['s'])
|
|
38
|
+
|
|
29
39
|
|
|
30
40
|
class Post(ITDBaseModel):
|
|
31
41
|
_validator = lambda _: _PostValidate
|
|
@@ -59,6 +69,8 @@ class Post(ITDBaseModel):
|
|
|
59
69
|
|
|
60
70
|
wall_recipient_id: UUID | None = Field(None, alias='wallRecipientId')
|
|
61
71
|
wall_recipient: User | None = Field(None, alias='wallRecipient')
|
|
72
|
+
# vs: ViewerSession
|
|
73
|
+
vs: str # from 13.05 it is string token
|
|
62
74
|
|
|
63
75
|
|
|
64
76
|
def __init__(self, id: str | UUID, client: Client | None = None) -> None:
|
|
@@ -314,13 +326,13 @@ class Post(ITDBaseModel):
|
|
|
314
326
|
def url(self) -> str:
|
|
315
327
|
return f'https://xn--d1ah4a.com/@{self.author.username}/post/{self.id}'
|
|
316
328
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
329
|
+
if not TYPE_CHECKING:
|
|
330
|
+
def __getattribute__(self, name: str):
|
|
331
|
+
value = super().__getattribute__(name)
|
|
332
|
+
if name == 'comments' and getattr(value, '_post_id', None) is None:
|
|
333
|
+
value = Comments()
|
|
334
|
+
value._post_id = self.id
|
|
335
|
+
return value
|
|
324
336
|
|
|
325
337
|
|
|
326
338
|
|
|
@@ -371,6 +383,11 @@ class _PostValidate(BaseModel, Post): # BaseModel MUST be first or you ll have s
|
|
|
371
383
|
if wall_recipient is not None:
|
|
372
384
|
return User._from_dict(wall_recipient, False)
|
|
373
385
|
|
|
386
|
+
# @field_validator('vs', mode='plain')
|
|
387
|
+
# @classmethod
|
|
388
|
+
# def validate_vs(cls, vs: str):
|
|
389
|
+
# return ViewerSession(decode_jwt_payload(vs))
|
|
390
|
+
|
|
374
391
|
|
|
375
392
|
|
|
376
393
|
class _BasePosts(ITDList[Post]):
|
|
@@ -106,10 +106,11 @@ class Privacy(ITDBaseModel):
|
|
|
106
106
|
instance._user = user
|
|
107
107
|
return instance
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
if not TYPE_CHECKING:
|
|
110
|
+
def __getattribute__(self, name: str):
|
|
111
|
+
if name in ('is_private', 'wall_access', 'likes_visibility') and object.__getattribute__(self, '_user'):
|
|
112
|
+
setattr(self, name, getattr(object.__getattribute__(self, '_user'), name))
|
|
113
|
+
return super().__getattribute__(name)
|
|
113
114
|
|
|
114
115
|
|
|
115
116
|
class _PrivacyValidate(BaseModel, Privacy):
|
|
@@ -515,11 +516,12 @@ class Me(_UserBase):
|
|
|
515
516
|
self._pins = [Pin(pin, self) for pin in get_pins(self.client).json()['data']['pins']]
|
|
516
517
|
return self._pins
|
|
517
518
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
value
|
|
522
|
-
|
|
519
|
+
if not TYPE_CHECKING:
|
|
520
|
+
def __getattribute__(self, name: str):
|
|
521
|
+
value = super().__getattribute__(name)
|
|
522
|
+
if name == 'pin' and value is not None and getattr(value, '_user', None) is None:
|
|
523
|
+
value._user = self
|
|
524
|
+
return value
|
|
523
525
|
|
|
524
526
|
|
|
525
527
|
|
|
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
|