itd-sdk 2.9.1__tar.gz → 2.9.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.9.1 → itd_sdk-2.9.2}/PKG-INFO +1 -1
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/__init__.py +6 -2
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/auth.py +10 -6
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/client.py +18 -13
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/config.py +7 -1
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/profile.py +10 -7
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/qr.py +40 -12
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/request.py +4 -2
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/utils.py +0 -16
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/enums.py +1 -1
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/utils.py +6 -5
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/pyproject.toml +5 -1
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/README.md +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/__init__.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/auth.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/comments.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/dwell.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/files.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/hashtags.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/notifications.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/pins.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/platform.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/polls.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/portal.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/posts.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/reports.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/search.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/sessions.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/subscription.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/users.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/api/verification.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/__init__.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/base.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/captcha/__init__.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/captcha/base.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/captcha/cloudflare.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/captcha/itd.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/cli.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/default.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/dwell.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/limiter.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/logger.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/timer.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/core/visibility.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/exceptions.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/__init__.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/announcement.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/clan.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/comment.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/file.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/hashtag.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/notification.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/pin.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/poll.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/portal.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/post.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/report.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/search.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/session.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/span.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/user.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/models/version.py +0 -0
- {itd_sdk-2.9.1 → itd_sdk-2.9.2}/itd/py.typed +0 -0
|
@@ -37,7 +37,7 @@ from time import sleep
|
|
|
37
37
|
__version__ = version('itd-sdk')
|
|
38
38
|
|
|
39
39
|
from itd.core.client import Client as ITDClient
|
|
40
|
-
from itd.core.client import init_client
|
|
40
|
+
from itd.core.client import init_client, init_not_authed_client
|
|
41
41
|
from itd.core.config import Config as ITDConfig
|
|
42
42
|
from itd.core.default import LimiterConfig, limiters
|
|
43
43
|
from itd.core.default import set_config as set_limiter_config
|
|
@@ -51,8 +51,9 @@ from itd.models.notification import Notification, Notifications, Ntf, Ntfs
|
|
|
51
51
|
from itd.models.poll import NewPoll
|
|
52
52
|
from itd.models.portal import Portal
|
|
53
53
|
from itd.models.post import HashtagPosts, LikedPosts, Post, Posts, UserPosts
|
|
54
|
+
from itd.models.search import Search
|
|
54
55
|
from itd.models.session import Sessions
|
|
55
|
-
from itd.models.user import Me, User, Users, get_follow_status
|
|
56
|
+
from itd.models.user import Me, User, Users, WhoToFollow, get_follow_status
|
|
56
57
|
from itd.models.version import Apps, Changelog
|
|
57
58
|
|
|
58
59
|
|
|
@@ -71,6 +72,7 @@ __all__ = [
|
|
|
71
72
|
'ITDClient',
|
|
72
73
|
'ITDConfig',
|
|
73
74
|
'init_client',
|
|
75
|
+
'init_not_authed_client',
|
|
74
76
|
'Comment',
|
|
75
77
|
'Clan',
|
|
76
78
|
'TopClans',
|
|
@@ -94,6 +96,8 @@ __all__ = [
|
|
|
94
96
|
'Me',
|
|
95
97
|
'Users',
|
|
96
98
|
'Portal',
|
|
99
|
+
'WhoToFollow',
|
|
100
|
+
'Search',
|
|
97
101
|
'get_follow_status',
|
|
98
102
|
'set_limiter_config',
|
|
99
103
|
'LimiterConfig',
|
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
from itd.api.auth import sign_in
|
|
7
7
|
from itd.core.captcha import get_turnstile
|
|
8
8
|
from itd.core.logger import RICH_AVAILABLE, get_logger, iprint, rich_input
|
|
9
|
-
from itd.core.qr import
|
|
9
|
+
from itd.core.qr import interactive_auth_qr
|
|
10
10
|
from itd.core.utils import shorten_token
|
|
11
11
|
from itd.exceptions import (
|
|
12
12
|
AccessTokenExpiredError,
|
|
@@ -114,7 +114,7 @@ class AccessAuth:
|
|
|
114
114
|
AuthMethod = CredentialsAuth | RefreshAuth | AccessAuth
|
|
115
115
|
|
|
116
116
|
|
|
117
|
-
def apply_auth(client: 'Client', auth: AuthMethod) -> None:
|
|
117
|
+
def apply_auth(client: 'Client', auth: AuthMethod | None) -> None:
|
|
118
118
|
match auth:
|
|
119
119
|
case CredentialsAuth():
|
|
120
120
|
client._profile.email = auth.email
|
|
@@ -158,7 +158,6 @@ def apply_auth(client: 'Client', auth: AuthMethod) -> None:
|
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
def env_auth(client: 'Client') -> bool:
|
|
161
|
-
"""Auth from ITD_* environment variables. Returns False if none set."""
|
|
162
161
|
method = getenv('ITD_AUTH_METHOD')
|
|
163
162
|
if not method:
|
|
164
163
|
return False
|
|
@@ -170,6 +169,8 @@ def env_auth(client: 'Client') -> bool:
|
|
|
170
169
|
auth = RefreshAuth(getenv('ITD_REFRESH', ''))
|
|
171
170
|
case 'access':
|
|
172
171
|
auth = AccessAuth(getenv('ITD_ACCESS', ''))
|
|
172
|
+
case 'no':
|
|
173
|
+
auth = None
|
|
173
174
|
case _:
|
|
174
175
|
raise ValueError(f'unknown env auth method {method}')
|
|
175
176
|
|
|
@@ -208,7 +209,8 @@ def interactive_auth(client: 'Client') -> bool:
|
|
|
208
209
|
iprint(l, '[2] Login via QR code')
|
|
209
210
|
iprint(l, '[3] Manually auth using refresh token')
|
|
210
211
|
iprint(l, '[4] Manually auth using access token')
|
|
211
|
-
iprint(l, '[5]
|
|
212
|
+
iprint(l, '[5] Init client with authorization')
|
|
213
|
+
iprint(l, '[6] Quit')
|
|
212
214
|
|
|
213
215
|
while True:
|
|
214
216
|
option = rich_input('option', 'magenta')
|
|
@@ -218,7 +220,7 @@ def interactive_auth(client: 'Client') -> bool:
|
|
|
218
220
|
if _auth_login(client, rich_input('email', 'green'), rich_input('password', 'green', password=True)):
|
|
219
221
|
return True
|
|
220
222
|
case '2':
|
|
221
|
-
if
|
|
223
|
+
if interactive_auth_qr(client):
|
|
222
224
|
return True
|
|
223
225
|
case '3':
|
|
224
226
|
if _auth_refresh(client, rich_input('refresh token', 'cyan', password=True)):
|
|
@@ -227,7 +229,9 @@ def interactive_auth(client: 'Client') -> bool:
|
|
|
227
229
|
l.info('note: authorization will work only for ~15min')
|
|
228
230
|
if _auth_access(client, rich_input('access token', 'cyan')):
|
|
229
231
|
return True
|
|
230
|
-
case '5'
|
|
232
|
+
case '5':
|
|
233
|
+
return True
|
|
234
|
+
case '6' | 'q' | 'quit':
|
|
231
235
|
quit()
|
|
232
236
|
case _:
|
|
233
237
|
l.error('unknown option')
|
|
@@ -20,7 +20,6 @@ from itd.core.dwell import DwellTracker
|
|
|
20
20
|
from itd.core.logger import get_logger
|
|
21
21
|
from itd.core.profile import Profile, clear_anon_profile
|
|
22
22
|
from itd.core.request import fetch
|
|
23
|
-
from itd.core.utils import get_profile
|
|
24
23
|
from itd.core.visibility import VisibilityTracker
|
|
25
24
|
from itd.enums import AuthLevel
|
|
26
25
|
from itd.exceptions import AccessTokenExpiredError, InsufficientAuthLevelError, SessionExpiredError
|
|
@@ -37,27 +36,27 @@ class Client:
|
|
|
37
36
|
l.info('init client %s', name)
|
|
38
37
|
self.config = config or Config()
|
|
39
38
|
|
|
40
|
-
self.auth_level: AuthLevel = AuthLevel.NO
|
|
41
|
-
self._profile: Profile = get_profile(name)
|
|
42
|
-
|
|
43
|
-
self._refresh_lock = RLock() # so background timers and main thread dont refresh token simultaneously # еба он мой стиль коментов спиздил
|
|
44
|
-
|
|
45
39
|
self.session = Session()
|
|
46
40
|
adapter = HTTPAdapter(pool_connections=1, pool_maxsize=10, pool_block=False) # idk what is this, (claude added) just for better stability
|
|
47
41
|
self.session.mount('https://', adapter)
|
|
48
42
|
|
|
43
|
+
self.auth_level: AuthLevel = AuthLevel.NO
|
|
44
|
+
self._profile: Profile = Profile.get(name)
|
|
45
|
+
self._set_from_profile()
|
|
46
|
+
|
|
47
|
+
self._refresh_lock = RLock() # so background timers and main thread dont refresh token simultaneously # еба он мой стиль коментов спиздил
|
|
48
|
+
|
|
49
49
|
if maybe_get_default_client() is None or self.config.is_default:
|
|
50
50
|
set_default_client(self)
|
|
51
51
|
|
|
52
52
|
self._credtest = True
|
|
53
53
|
if auth is not None:
|
|
54
54
|
apply_auth(self, auth)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
self._set_from_profile()
|
|
55
|
+
if name != 'no-auth' and self.auth_level == AuthLevel.NO:
|
|
56
|
+
if not env_auth(self) and self.config._interactive_auth:
|
|
57
|
+
interactive_auth(self)
|
|
60
58
|
self._profile.flush()
|
|
59
|
+
self._credtest = False
|
|
61
60
|
|
|
62
61
|
self.dwell_tracker = DwellTracker(self)
|
|
63
62
|
self.dwell_tracker.start()
|
|
@@ -137,6 +136,7 @@ class Client:
|
|
|
137
136
|
|
|
138
137
|
if level >= AuthLevel.REFRESH and ((self._profile.refresh and self._profile.is_refresh_expired) or not self._profile.refresh_valid):
|
|
139
138
|
self._profile.refresh_valid = False
|
|
139
|
+
self._profile.flush()
|
|
140
140
|
if self.auth_level == AuthLevel.LOGIN:
|
|
141
141
|
self.login()
|
|
142
142
|
else:
|
|
@@ -150,6 +150,7 @@ class Client:
|
|
|
150
150
|
and ((self._profile.access_data and self._profile.access_data.is_expired) or not self._profile.access_valid)
|
|
151
151
|
):
|
|
152
152
|
self._profile.access_valid = False
|
|
153
|
+
self._profile.flush()
|
|
153
154
|
if self.auth_level >= AuthLevel.REFRESH:
|
|
154
155
|
self.refresh_auth()
|
|
155
156
|
else:
|
|
@@ -214,8 +215,12 @@ class Client:
|
|
|
214
215
|
change_password(self, old, new)
|
|
215
216
|
|
|
216
217
|
|
|
217
|
-
def init_client(name: str | None = None, config: Config =
|
|
218
|
+
def init_client(name: str | None = None, config: Config | None = None, auth: AuthMethod | None = None) -> Client:
|
|
218
219
|
if name == 'anon':
|
|
219
220
|
clear_anon_profile()
|
|
220
221
|
register(clear_anon_profile)
|
|
221
|
-
return Client(name or 'default', config=config)
|
|
222
|
+
return Client(name or 'default', config=config, auth=auth)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def init_not_authed_client(config: Config | None = None):
|
|
226
|
+
return Client('no-auth', config=config)
|
|
@@ -114,7 +114,8 @@ class Config:
|
|
|
114
114
|
on_exceptions: dict[type[Exception], Callable[[Exception], None]] = field(default_factory=dict)
|
|
115
115
|
batch_sizes: BatchSizes = field(default_factory=BatchSizes)
|
|
116
116
|
refresh_token_cookie_name: str = 'refresh_token'
|
|
117
|
-
|
|
117
|
+
|
|
118
|
+
interactive_auth: bool | None = None
|
|
118
119
|
|
|
119
120
|
def __post_init__(self):
|
|
120
121
|
match self.user_agent:
|
|
@@ -162,3 +163,8 @@ class Config:
|
|
|
162
163
|
self._retry_exceptions = ()
|
|
163
164
|
else:
|
|
164
165
|
self._retry_exceptions = self.retry_exceptions
|
|
166
|
+
|
|
167
|
+
if self.interactive_auth is None:
|
|
168
|
+
self._interactive_auth = self.client_type != 'client'
|
|
169
|
+
else:
|
|
170
|
+
self._interactive_auth = self.interactive_auth
|
|
@@ -51,16 +51,18 @@ class Profile(BaseModel):
|
|
|
51
51
|
def flush(self):
|
|
52
52
|
assert self._file
|
|
53
53
|
self.updated_at = datetime.now()
|
|
54
|
+
if self._file.name == 'no-auth':
|
|
55
|
+
return
|
|
54
56
|
self._file.write_text(dumps(self.model_dump(mode='json')))
|
|
55
57
|
|
|
56
58
|
def update(self):
|
|
57
59
|
assert self._file
|
|
60
|
+
if self._file.name == 'no-auth':
|
|
61
|
+
return False
|
|
58
62
|
new = self.model_validate(loads(self._file.read_text()))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return True
|
|
63
|
-
return False
|
|
63
|
+
for field in Profile.model_fields:
|
|
64
|
+
setattr(self, field, getattr(new, field))
|
|
65
|
+
return True
|
|
64
66
|
|
|
65
67
|
@property
|
|
66
68
|
def is_refresh_expired(self) -> bool:
|
|
@@ -88,9 +90,10 @@ class Profile(BaseModel):
|
|
|
88
90
|
@classmethod
|
|
89
91
|
def get(cls, name: str):
|
|
90
92
|
file = user_data_path('itd_sdk', False, ensure_exists=True) / f'{name}.json'
|
|
91
|
-
instance = cls(
|
|
93
|
+
instance = cls()
|
|
94
|
+
instance._file = file
|
|
92
95
|
if not file.exists():
|
|
93
|
-
instance
|
|
96
|
+
instance.flush()
|
|
94
97
|
else:
|
|
95
98
|
instance.update()
|
|
96
99
|
|
|
@@ -11,6 +11,7 @@ from itd.core.logger import RICH_AVAILABLE, get_logger, iprint
|
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from itd.core.client import Client
|
|
14
|
+
from itd.core.config import Config
|
|
14
15
|
|
|
15
16
|
try:
|
|
16
17
|
from qrcode import QRCode
|
|
@@ -35,10 +36,14 @@ class ITDQRCode(BaseModel):
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class QRLogin:
|
|
38
|
-
def __init__(self
|
|
39
|
-
|
|
39
|
+
def __init__(self):
|
|
40
|
+
from itd import init_not_authed_client
|
|
41
|
+
|
|
42
|
+
self.client = init_not_authed_client()
|
|
40
43
|
self.stream = None
|
|
41
44
|
self.qr = None
|
|
45
|
+
self.auth = None
|
|
46
|
+
self.status = 'pending'
|
|
42
47
|
self.refresh()
|
|
43
48
|
|
|
44
49
|
def refresh(self) -> ITDQRCode:
|
|
@@ -53,21 +58,41 @@ class QRLogin:
|
|
|
53
58
|
l.debug('start stream')
|
|
54
59
|
try:
|
|
55
60
|
for event in SSEClient(self.stream).events():
|
|
56
|
-
status = loads(event.data)['status']
|
|
57
|
-
l.debug('qr code status: %s', status)
|
|
58
|
-
yield status
|
|
59
|
-
if status in ('approved', 'rejected'):
|
|
60
|
-
|
|
61
|
+
self.status = loads(event.data)['status']
|
|
62
|
+
l.debug('qr code status: %s', self.status)
|
|
63
|
+
yield self.status
|
|
64
|
+
if self.status in ('approved', 'rejected'):
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
if self.status == 'approved':
|
|
68
|
+
self.claim()
|
|
69
|
+
self.status = 'authorized'
|
|
70
|
+
yield 'authorized'
|
|
71
|
+
elif self.status != 'rejected':
|
|
72
|
+
l.debug('expire qr')
|
|
73
|
+
self.status = 'expired'
|
|
74
|
+
yield 'expired'
|
|
61
75
|
|
|
62
76
|
finally:
|
|
63
77
|
self.close()
|
|
64
78
|
l.debug('stop stream')
|
|
65
79
|
|
|
66
80
|
def claim(self):
|
|
81
|
+
from itd.core.auth import RefreshAuth
|
|
82
|
+
|
|
67
83
|
assert self.qr
|
|
68
84
|
res = qr_claim(self.client, qr_id=self.qr.id, claim_token=self.qr.claim_token)
|
|
69
|
-
self.
|
|
70
|
-
|
|
85
|
+
self.auth = RefreshAuth(res.cookies['refresh_token'], res.json()['accessToken'])
|
|
86
|
+
|
|
87
|
+
def get_client(self, name: str | None = None, config: 'Config | None' = None):
|
|
88
|
+
from itd import ITDConfig, init_client
|
|
89
|
+
|
|
90
|
+
if config is None:
|
|
91
|
+
config = ITDConfig()
|
|
92
|
+
config.is_default = True
|
|
93
|
+
|
|
94
|
+
assert self.auth
|
|
95
|
+
return init_client(name, config, auth=self.auth)
|
|
71
96
|
|
|
72
97
|
def close(self):
|
|
73
98
|
if self.stream:
|
|
@@ -81,7 +106,7 @@ class QRLogin:
|
|
|
81
106
|
self.close()
|
|
82
107
|
|
|
83
108
|
|
|
84
|
-
def
|
|
109
|
+
def interactive_auth_qr(client: 'Client'):
|
|
85
110
|
if not QR_AVAILABLE:
|
|
86
111
|
l.error(r'qrcode library not installed; install via `uv add itd-sdk\[qrcode]`')
|
|
87
112
|
return False
|
|
@@ -92,7 +117,7 @@ def auth_qr(client: 'Client'):
|
|
|
92
117
|
else:
|
|
93
118
|
status = None
|
|
94
119
|
|
|
95
|
-
with QRLogin(
|
|
120
|
+
with QRLogin() as qr:
|
|
96
121
|
try:
|
|
97
122
|
for _ in range(5):
|
|
98
123
|
qr.refresh()
|
|
@@ -109,7 +134,10 @@ def auth_qr(client: 'Client'):
|
|
|
109
134
|
for event in qr.events():
|
|
110
135
|
if event == 'approved':
|
|
111
136
|
iprint(l, 'qr code approved')
|
|
112
|
-
qr.
|
|
137
|
+
assert qr.auth
|
|
138
|
+
assert qr.auth.access
|
|
139
|
+
client._profile.set_access(qr.auth.access)
|
|
140
|
+
client._profile.set_refresh(qr.auth.refresh, set_expire=True)
|
|
113
141
|
return True
|
|
114
142
|
|
|
115
143
|
elif event == 'rejected':
|
|
@@ -241,6 +241,7 @@ def api_wrapper(*exceptions: ITDException):
|
|
|
241
241
|
# token is checked before the request, but server still can reject it (clock skew, revoked session) - refresh and repeat the request once, before callbacks
|
|
242
242
|
if isinstance(exception, (AccessTokenExpiredError, InvalidAccessTokenError)) and not access_reauthed and not client._credtest:
|
|
243
243
|
client._profile.access_valid = False
|
|
244
|
+
client._profile.flush()
|
|
244
245
|
if name != 'refresh_token':
|
|
245
246
|
access_reauthed = True
|
|
246
247
|
l.warning('%s on %s: refresh access_token and retry', exception.__class__.__name__, name)
|
|
@@ -252,9 +253,10 @@ def api_wrapper(*exceptions: ITDException):
|
|
|
252
253
|
and not refresh_reauthed
|
|
253
254
|
and not client._credtest
|
|
254
255
|
):
|
|
255
|
-
|
|
256
|
+
client._profile.refresh_valid = False
|
|
257
|
+
client._profile.flush()
|
|
256
258
|
if name != 'sign_in':
|
|
257
|
-
|
|
259
|
+
refresh_reauthed = True
|
|
258
260
|
l.warning('%s on %s: refresh refresh_token and retry', exception.__class__.__name__, name)
|
|
259
261
|
client.login()
|
|
260
262
|
return exec()
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from json import dumps, loads
|
|
3
2
|
from sys import version
|
|
4
3
|
from uuid import UUID
|
|
5
4
|
|
|
6
|
-
from platformdirs import user_data_path
|
|
7
|
-
|
|
8
|
-
from itd.core.profile import Profile
|
|
9
|
-
|
|
10
5
|
|
|
11
6
|
def get_sdk_user_agent():
|
|
12
7
|
from itd import __version__ # i fucking hate circular imports this is sooo stupid
|
|
@@ -14,17 +9,6 @@ def get_sdk_user_agent():
|
|
|
14
9
|
return f'itd-sdk/{__version__} (Python/{version})'
|
|
15
10
|
|
|
16
11
|
|
|
17
|
-
def get_profile(name: str) -> Profile:
|
|
18
|
-
file = user_data_path('itd_sdk', False, ensure_exists=True) / f'{name}.json'
|
|
19
|
-
if not file.exists():
|
|
20
|
-
model = Profile(_file=file)
|
|
21
|
-
file.write_text(dumps(model.model_dump(mode='json')))
|
|
22
|
-
else:
|
|
23
|
-
model = Profile.model_validate(loads(file.read_text()))
|
|
24
|
-
model._file = file
|
|
25
|
-
return model
|
|
26
|
-
|
|
27
|
-
|
|
28
12
|
def shorten_token(value: str | None, max_len: int = 20):
|
|
29
13
|
if value is None:
|
|
30
14
|
return None
|
|
@@ -58,10 +58,11 @@ def parse_html(text: str) -> tuple[str, list[Span]]:
|
|
|
58
58
|
"""Спарсить HTML
|
|
59
59
|
|
|
60
60
|
Поддерживаемые теги:
|
|
61
|
-
-
|
|
61
|
+
- `<b>, <i>, <s>, <u>, <code>, <spoiler>, <q>`
|
|
62
|
+
- `<a href="url">text</a>` или `<а>url</а>` (ссылки)
|
|
62
63
|
|
|
63
64
|
Args:
|
|
64
|
-
text: HTML-строка для парсинга
|
|
65
|
+
text (str): HTML-строка для парсинга
|
|
65
66
|
|
|
66
67
|
Returns:
|
|
67
68
|
str: чистая строка
|
|
@@ -84,18 +85,18 @@ def parse_html(text: str) -> tuple[str, list[Span]]:
|
|
|
84
85
|
return root.text_content(), spans
|
|
85
86
|
|
|
86
87
|
|
|
87
|
-
def parse_md(
|
|
88
|
+
def parse_md(text: str) -> tuple[str, list[Span]]:
|
|
88
89
|
"""Спарсить markdown
|
|
89
90
|
|
|
90
91
|
Поддерживаемые теги:
|
|
91
92
|
- *, _, **, __, ~, ~~, `, ||, \\[text](url)
|
|
92
93
|
|
|
93
94
|
Args:
|
|
94
|
-
text: строка для парсинга
|
|
95
|
+
text (str): строка для парсинга
|
|
95
96
|
|
|
96
97
|
Returns:
|
|
97
98
|
str: чистая строка
|
|
98
99
|
list[Span]: список спанов
|
|
99
100
|
"""
|
|
100
|
-
text, spans = convert(
|
|
101
|
+
text, spans = convert(text, latex_escape=False)
|
|
101
102
|
return text, [Span.model_validate(span, from_attributes=True) for span in spans]
|
|
@@ -14,7 +14,7 @@ version_provider = "uv"
|
|
|
14
14
|
|
|
15
15
|
[project]
|
|
16
16
|
name = "itd-sdk"
|
|
17
|
-
version = "2.9.
|
|
17
|
+
version = "2.9.2"
|
|
18
18
|
description = "ITD client for python"
|
|
19
19
|
readme = "README.md"
|
|
20
20
|
authors = [
|
|
@@ -66,6 +66,10 @@ captcha = [
|
|
|
66
66
|
dev = [
|
|
67
67
|
"pytest>=8.4.2",
|
|
68
68
|
"python-dotenv>=1.2.1",
|
|
69
|
+
"griffe-typingdoc>=0.3.0",
|
|
70
|
+
"mkdocs>=1.6.1",
|
|
71
|
+
"mkdocs-material>=9.7.6",
|
|
72
|
+
"mkdocstrings[python]>=0.30.1"
|
|
69
73
|
]
|
|
70
74
|
|
|
71
75
|
[project.scripts]
|
|
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
|
|
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
|