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.
Files changed (38) hide show
  1. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/PKG-INFO +1 -1
  2. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/auth.py +5 -4
  3. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/hashtags.py +3 -2
  4. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/search.py +2 -1
  5. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/base.py +8 -5
  6. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/client.py +33 -9
  7. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/enums.py +25 -5
  8. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/exceptions.py +7 -3
  9. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/post.py +30 -13
  10. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/user.py +11 -9
  11. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/pyproject.toml +1 -1
  12. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/README.md +0 -0
  13. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/__init__.py +0 -0
  14. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/_default.py +0 -0
  15. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/__init__.py +0 -0
  16. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/comments.py +0 -0
  17. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/etc.py +0 -0
  18. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/files.py +0 -0
  19. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/notifications.py +0 -0
  20. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/pins.py +0 -0
  21. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/polls.py +0 -0
  22. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/posts.py +0 -0
  23. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/reports.py +0 -0
  24. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/subscription.py +0 -0
  25. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/users.py +0 -0
  26. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/api/verification.py +0 -0
  27. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/clan.py +0 -0
  28. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/comment.py +0 -0
  29. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/file.py +0 -0
  30. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/hashtag.py +0 -0
  31. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/logger.py +0 -0
  32. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/notification.py +0 -0
  33. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/pin.py +0 -0
  34. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/poll.py +0 -0
  35. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/report.py +0 -0
  36. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/request.py +0 -0
  37. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/span.py +0 -0
  38. {itd_sdk-2.1.1 → itd_sdk-2.2.0}/itd/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itd-sdk
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: ITD client for python
5
5
  Keywords: itd,itdsocial
6
6
  Author: firedotguy
@@ -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.request import fetch
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 fetch(client, 'post', 'v1/auth/refresh')
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 RateLimitError as e:
377
- l.info('rate limit on %s; wait %ss', func.__name__, e.retry_after or 10)
378
- sleep(e.retry_after or 10)
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
- self.refresh_auth()
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 _HIERARCHY.index(self) > _HIERARCHY.index(other)
104
+ return _ACCESS_TYPE_HIERARCHY.index(self) > _ACCESS_TYPE_HIERARCHY.index(other)
85
105
 
86
106
  def __lt__(self, other):
87
- return _HIERARCHY.index(self) < _HIERARCHY.index(other)
107
+ return _ACCESS_TYPE_HIERARCHY.index(self) < _ACCESS_TYPE_HIERARCHY.index(other)
88
108
 
89
109
  def __ge__(self, other):
90
- return _HIERARCHY.index(self) >= _HIERARCHY.index(other)
110
+ return _ACCESS_TYPE_HIERARCHY.index(self) >= _ACCESS_TYPE_HIERARCHY.index(other)
91
111
 
92
112
  def __le__(self, other):
93
- return _HIERARCHY.index(self) <= _HIERARCHY.index(other)
113
+ return _ACCESS_TYPE_HIERARCHY.index(self) <= _ACCESS_TYPE_HIERARCHY.index(other)
94
114
 
95
- _HIERARCHY = [AccessType.EVERYONE, AccessType.FOLLOWERS, AccessType.MUTUAL, AccessType.EVERYONE] # 100% that hierarcy is spelled wrong
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
- from itd.utils import to_uuid, parse_datetime, format_attachments, ATTACHMENTS, parse_html, parse_md
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
- def __getattribute__(self, name: str):
319
- value = super().__getattribute__(name)
320
- if name == 'comments' and getattr(value, '_post_id', None) is None:
321
- value = Comments()
322
- value._post_id = self.id
323
- return value
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
- def __getattribute__(self, name: str):
110
- if name in ('is_private', 'wall_access', 'likes_visibility') and object.__getattribute__(self, '_user'):
111
- setattr(self, name, getattr(object.__getattribute__(self, '_user'), name))
112
- return super().__getattribute__(name)
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
- def __getattribute__(self, name: str):
519
- value = super().__getattribute__(name)
520
- if name == 'pin' and value is not None and getattr(value, '_user', None) is None:
521
- value._user = self
522
- return value
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
 
@@ -14,7 +14,7 @@ version_provider = "uv"
14
14
 
15
15
  [project]
16
16
  name = "itd-sdk"
17
- version = "2.1.1"
17
+ version = "2.2.0"
18
18
  description = "ITD client for python"
19
19
  readme = "README.md"
20
20
  authors = [
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