itd-sdk 2.1.2__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.2 → itd_sdk-2.2.0}/PKG-INFO +1 -1
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/auth.py +5 -4
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/hashtags.py +3 -2
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/search.py +2 -1
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/base.py +4 -5
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/client.py +33 -11
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/enums.py +25 -5
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/exceptions.py +6 -3
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/post.py +22 -4
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/pyproject.toml +1 -1
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/README.md +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/__init__.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/_default.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/__init__.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/comments.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/etc.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/files.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/notifications.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/pins.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/polls.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/posts.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/reports.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/subscription.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/users.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/api/verification.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/clan.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/comment.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/file.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/hashtag.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/logger.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/notification.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/pin.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/poll.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/report.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/request.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/span.py +0 -0
- {itd_sdk-2.1.2 → itd_sdk-2.2.0}/itd/user.py +0 -0
- {itd_sdk-2.1.2 → 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)
|
|
@@ -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,15 +369,15 @@ 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
|
|
|
373
|
-
if not client.config.
|
|
372
|
+
if not client.config.retry_enabled:
|
|
374
373
|
return func(client, *args, **kwargs)
|
|
375
374
|
|
|
376
375
|
while True:
|
|
377
376
|
try:
|
|
378
377
|
return func(client, *args, **kwargs)
|
|
379
|
-
except (
|
|
380
|
-
retry_after = getattr(e, 'retry_after', client.config.rate_limit_wait) or client.config.
|
|
381
|
-
l.info('
|
|
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)
|
|
382
381
|
sleep(retry_after)
|
|
383
382
|
|
|
384
383
|
return wrapper
|
|
@@ -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,8 +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
|
|
48
|
-
rate_limit_wait: int =
|
|
49
|
-
retry_on_rate_limits: bool =
|
|
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
|
|
50
56
|
|
|
51
57
|
def __post_init__(self):
|
|
52
58
|
if self.rate_limit_default:
|
|
@@ -73,10 +79,19 @@ class Config:
|
|
|
73
79
|
case _:
|
|
74
80
|
self._user_agent = self.user_agent
|
|
75
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)
|
|
76
90
|
|
|
77
91
|
|
|
78
92
|
|
|
79
93
|
class Client:
|
|
94
|
+
auth_level: AuthLevel = AuthLevel.NO
|
|
80
95
|
access_token: str | None = None
|
|
81
96
|
refresh_token: str | None = None
|
|
82
97
|
_user = None
|
|
@@ -92,18 +107,20 @@ class Client:
|
|
|
92
107
|
self.session.mount('https://', adapter)
|
|
93
108
|
|
|
94
109
|
if access:
|
|
110
|
+
self.auth_level = AuthLevel.ACCESS
|
|
95
111
|
self.access_token = access.replace('Bearer ', '')
|
|
96
112
|
|
|
97
113
|
if refresh:
|
|
114
|
+
self.auth_level = AuthLevel.REFRESH
|
|
98
115
|
self.refresh_token = refresh
|
|
99
116
|
self.session.cookies.set('refresh_token', refresh, path='/', domain=self.config.url)
|
|
100
|
-
if access is None:
|
|
101
|
-
|
|
117
|
+
# if access is None:
|
|
118
|
+
# self.refresh_auth()
|
|
102
119
|
|
|
103
120
|
if _default_client is None or config.is_default:
|
|
104
121
|
set_default_client(self)
|
|
105
122
|
|
|
106
|
-
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):
|
|
107
124
|
"""Сделать запрос
|
|
108
125
|
|
|
109
126
|
Args:
|
|
@@ -112,11 +129,18 @@ class Client:
|
|
|
112
129
|
params (dict, optional): Параметры. Defaults to {}.
|
|
113
130
|
files (dict[str, tuple[str, BufferedReader | bytes]], optional): Файлы. Defaults to {}.
|
|
114
131
|
"""
|
|
115
|
-
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
|
+
|
|
116
140
|
def _fetch():
|
|
117
141
|
return fetch(self, method, url, params, files)
|
|
118
142
|
|
|
119
|
-
if not self.refresh_token:
|
|
143
|
+
if not self.refresh_token or url == 'v1/auth/refresh':
|
|
120
144
|
return _fetch()
|
|
121
145
|
|
|
122
146
|
try:
|
|
@@ -132,8 +156,6 @@ class Client:
|
|
|
132
156
|
str: Токен
|
|
133
157
|
"""
|
|
134
158
|
l.debug('refresh token')
|
|
135
|
-
if not self.refresh_token:
|
|
136
|
-
raise InsufficientAuthLevelError()
|
|
137
159
|
|
|
138
160
|
res = refresh_token(self)
|
|
139
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"
|
|
@@ -253,4 +256,4 @@ class ProfileRequiredError(ITDException):
|
|
|
253
256
|
text = 'No profile. Please create your profile first'
|
|
254
257
|
|
|
255
258
|
|
|
256
|
-
DEFAULT_ERRORS = (RateLimitError(), InvalidAccessTokenError(), UnauthorizedError(), AccessTokenExpiredError(), AccountBannedError(), InternalError(), ProfileRequiredError())
|
|
259
|
+
DEFAULT_ERRORS = (RateLimitError(), InvalidAccessTokenError(), UnauthorizedError(), AccessTokenExpiredError(), AccountBannedError(), InternalError(), ProfileRequiredError(), RefreshTokenMissingError())
|
|
@@ -5,26 +5,37 @@ from time import sleep
|
|
|
5
5
|
|
|
6
6
|
from pydantic import Field, BaseModel, field_validator
|
|
7
7
|
|
|
8
|
-
from itd.base import ITDBaseModel, refresh_wrapper, ITDList
|
|
9
|
-
from itd.client import Client
|
|
10
8
|
from itd.comment import Comment, Comments
|
|
11
|
-
from itd.enums import PostsTab, UserPostSorting, ReportReason, ReportTargetType, ParseMode, ALL
|
|
12
9
|
from itd.file import PostAttach
|
|
13
10
|
from itd.hashtag import Hashtag
|
|
14
11
|
from itd.poll import Poll, NewPoll, PollOption
|
|
15
12
|
from itd.report import Report
|
|
16
13
|
from itd.span import Span
|
|
17
14
|
from itd.user import User, _UserBase, Me
|
|
18
|
-
|
|
15
|
+
|
|
19
16
|
from itd.api.posts import (
|
|
20
17
|
get_post, create_post, like_post, unlike_post, repost, view_post, pin_post, unpin_post,
|
|
21
18
|
delete_post, restore_post, edit_post, get_posts, get_user_posts, get_liked_posts
|
|
22
19
|
)
|
|
23
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
|
|
24
24
|
from itd.logger import get_logger
|
|
25
|
+
from itd.utils import to_uuid, parse_datetime, format_attachments, ATTACHMENTS, parse_html, parse_md
|
|
25
26
|
|
|
26
27
|
l = get_logger('post')
|
|
27
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
|
+
|
|
28
39
|
|
|
29
40
|
class Post(ITDBaseModel):
|
|
30
41
|
_validator = lambda _: _PostValidate
|
|
@@ -58,6 +69,8 @@ class Post(ITDBaseModel):
|
|
|
58
69
|
|
|
59
70
|
wall_recipient_id: UUID | None = Field(None, alias='wallRecipientId')
|
|
60
71
|
wall_recipient: User | None = Field(None, alias='wallRecipient')
|
|
72
|
+
# vs: ViewerSession
|
|
73
|
+
vs: str # from 13.05 it is string token
|
|
61
74
|
|
|
62
75
|
|
|
63
76
|
def __init__(self, id: str | UUID, client: Client | None = None) -> None:
|
|
@@ -370,6 +383,11 @@ class _PostValidate(BaseModel, Post): # BaseModel MUST be first or you ll have s
|
|
|
370
383
|
if wall_recipient is not None:
|
|
371
384
|
return User._from_dict(wall_recipient, False)
|
|
372
385
|
|
|
386
|
+
# @field_validator('vs', mode='plain')
|
|
387
|
+
# @classmethod
|
|
388
|
+
# def validate_vs(cls, vs: str):
|
|
389
|
+
# return ViewerSession(decode_jwt_payload(vs))
|
|
390
|
+
|
|
373
391
|
|
|
374
392
|
|
|
375
393
|
class _BasePosts(ITDList[Post]):
|
|
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
|