cloudtips 0.1.1__tar.gz → 0.3.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.
- {cloudtips-0.1.1 → cloudtips-0.3.0}/PKG-INFO +1 -1
- {cloudtips-0.1.1 → cloudtips-0.3.0}/cloudtips/__init__.py +6 -2
- {cloudtips-0.1.1 → cloudtips-0.3.0}/cloudtips/client.py +138 -14
- cloudtips-0.3.0/cloudtips/models.py +161 -0
- {cloudtips-0.1.1 → cloudtips-0.3.0}/pyproject.toml +1 -1
- cloudtips-0.1.1/cloudtips/models.py +0 -48
- {cloudtips-0.1.1 → cloudtips-0.3.0}/.github/workflows/publish.yml +0 -0
- {cloudtips-0.1.1 → cloudtips-0.3.0}/LICENSE +0 -0
- {cloudtips-0.1.1 → cloudtips-0.3.0}/README.md +0 -0
- {cloudtips-0.1.1 → cloudtips-0.3.0}/cloudtips/auth.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudtips
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Неофициальная Python-библиотека для CloudTips (получение донатов, поллинг, обновление токенов)
|
|
5
5
|
Project-URL: Repository, https://github.com/IRRatium/cloudtips-api
|
|
6
6
|
Project-URL: Issues, https://github.com/IRRatium/cloudtips-api/issues
|
|
@@ -4,14 +4,18 @@ cloudtips — неофициальная Python-библиотека для Clou
|
|
|
4
4
|
|
|
5
5
|
from .auth import CloudTipsAuth, CloudTipsAuthError
|
|
6
6
|
from .client import CloudTipsAPIError, CloudTipsClient
|
|
7
|
-
from .models import Donation, TokenData
|
|
7
|
+
from .models import AccumulationSummary, Card, Donation, PayoutFeeInfo, ReceiverProfile, TokenData
|
|
8
8
|
|
|
9
|
-
__version__ = "0.
|
|
9
|
+
__version__ = "0.3.0"
|
|
10
10
|
__all__ = [
|
|
11
11
|
"CloudTipsAuth",
|
|
12
12
|
"CloudTipsAuthError",
|
|
13
13
|
"CloudTipsClient",
|
|
14
14
|
"CloudTipsAPIError",
|
|
15
15
|
"Donation",
|
|
16
|
+
"Card",
|
|
17
|
+
"PayoutFeeInfo",
|
|
18
|
+
"AccumulationSummary",
|
|
19
|
+
"ReceiverProfile",
|
|
16
20
|
"TokenData",
|
|
17
21
|
]
|
|
@@ -8,7 +8,7 @@ from typing import Callable, Iterator, List, Optional
|
|
|
8
8
|
import requests
|
|
9
9
|
|
|
10
10
|
from .auth import CloudTipsAuth
|
|
11
|
-
from .models import Donation
|
|
11
|
+
from .models import Donation, Card, PayoutFeeInfo, AccumulationSummary, ReceiverProfile
|
|
12
12
|
|
|
13
13
|
_BASE_URL = "https://api.cloudtips.ru/api"
|
|
14
14
|
_MSK = timezone(timedelta(hours=3))
|
|
@@ -38,12 +38,9 @@ class CloudTipsClient:
|
|
|
38
38
|
)
|
|
39
39
|
client = CloudTipsClient(auth)
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# Поллинг новых донатов каждые 30 секунд
|
|
45
|
-
for donation in client.poll(interval=30):
|
|
46
|
-
print(donation)
|
|
41
|
+
donations = client.get_all_donations()
|
|
42
|
+
cards = client.get_cards()
|
|
43
|
+
summary = client.get_accumulation_summary()
|
|
47
44
|
"""
|
|
48
45
|
|
|
49
46
|
def __init__(self, auth: CloudTipsAuth, base_url: str = _BASE_URL) -> None:
|
|
@@ -52,7 +49,7 @@ class CloudTipsClient:
|
|
|
52
49
|
self._session = requests.Session()
|
|
53
50
|
|
|
54
51
|
# ------------------------------------------------------------------
|
|
55
|
-
#
|
|
52
|
+
# Донаты
|
|
56
53
|
# ------------------------------------------------------------------
|
|
57
54
|
|
|
58
55
|
def get_donations(
|
|
@@ -130,9 +127,6 @@ class CloudTipsClient:
|
|
|
130
127
|
"""
|
|
131
128
|
Генератор: бесконечный поллинг новых донатов.
|
|
132
129
|
|
|
133
|
-
Каждые ``interval`` секунд опрашивает API и отдаёт только **новые**
|
|
134
|
-
донаты (те, что появились после последнего запроса).
|
|
135
|
-
|
|
136
130
|
Использование как генератора::
|
|
137
131
|
|
|
138
132
|
for donation in client.poll(interval=15):
|
|
@@ -149,7 +143,6 @@ class CloudTipsClient:
|
|
|
149
143
|
last_seen_ids: set = set()
|
|
150
144
|
cursor = _ensure_tz(since or datetime.now(_MSK))
|
|
151
145
|
|
|
152
|
-
# Первый запрос — запоминаем уже существующие, не отдаём как «новые»
|
|
153
146
|
for d in self.get_all_donations(since=cursor):
|
|
154
147
|
last_seen_ids.add(d.transaction_id)
|
|
155
148
|
|
|
@@ -159,7 +152,6 @@ class CloudTipsClient:
|
|
|
159
152
|
try:
|
|
160
153
|
fresh = self.get_all_donations(since=cursor)
|
|
161
154
|
except Exception as exc:
|
|
162
|
-
# Не падаем при временных ошибках, пробуем снова
|
|
163
155
|
print(f"[cloudtips] Ошибка при поллинге: {exc}")
|
|
164
156
|
continue
|
|
165
157
|
|
|
@@ -173,7 +165,117 @@ class CloudTipsClient:
|
|
|
173
165
|
yield donation
|
|
174
166
|
|
|
175
167
|
# ------------------------------------------------------------------
|
|
176
|
-
#
|
|
168
|
+
# Профиль
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def get_me(self) -> ReceiverProfile:
|
|
172
|
+
"""
|
|
173
|
+
Получить профиль текущего пользователя.
|
|
174
|
+
|
|
175
|
+
Содержит имя, телефон, метод выплат, лимиты сумм и другие данные.
|
|
176
|
+
|
|
177
|
+
:return: :class:`ReceiverProfile`
|
|
178
|
+
|
|
179
|
+
Пример::
|
|
180
|
+
|
|
181
|
+
me = client.get_me()
|
|
182
|
+
print(me.full_name) # IRRing
|
|
183
|
+
print(me.payout_method) # Accumulation
|
|
184
|
+
print(me.available_amount_min, me.available_amount_max) # 49.0 3000.0
|
|
185
|
+
"""
|
|
186
|
+
data = self._get("/receivers/me")
|
|
187
|
+
return ReceiverProfile.from_dict(data.get("data", {}))
|
|
188
|
+
|
|
189
|
+
# ------------------------------------------------------------------
|
|
190
|
+
# Карты
|
|
191
|
+
# ------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def get_cards(self) -> List[Card]:
|
|
194
|
+
"""
|
|
195
|
+
Получить список привязанных карт.
|
|
196
|
+
|
|
197
|
+
:return: список :class:`Card`
|
|
198
|
+
|
|
199
|
+
Пример::
|
|
200
|
+
|
|
201
|
+
for card in client.get_cards():
|
|
202
|
+
print(card) # MIR *3742 (T-BANK (TINKOFF), до 08/34) [по умолчанию]
|
|
203
|
+
print(card.token) # tk_89e6b3c6827afd4e9ccc36db2d22f
|
|
204
|
+
"""
|
|
205
|
+
data = self._get("/cards")
|
|
206
|
+
return [Card.from_dict(item) for item in data.get("data", [])]
|
|
207
|
+
|
|
208
|
+
def delete_card(self, card_token: str) -> bool:
|
|
209
|
+
"""
|
|
210
|
+
Удалить привязанную карту.
|
|
211
|
+
|
|
212
|
+
:param card_token: токен карты (``card.token``)
|
|
213
|
+
:return: ``True`` если удаление прошло успешно
|
|
214
|
+
|
|
215
|
+
Пример::
|
|
216
|
+
|
|
217
|
+
for card in client.get_cards():
|
|
218
|
+
client.delete_card(card.token)
|
|
219
|
+
"""
|
|
220
|
+
data = self._delete("/cards", json={"cardToken": card_token})
|
|
221
|
+
return data.get("succeed", False)
|
|
222
|
+
|
|
223
|
+
# ------------------------------------------------------------------
|
|
224
|
+
# Выплаты и баланс
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def get_payout_fee_info(self) -> PayoutFeeInfo:
|
|
228
|
+
"""
|
|
229
|
+
Получить информацию о комиссиях при выводе средств.
|
|
230
|
+
|
|
231
|
+
:return: :class:`PayoutFeeInfo`
|
|
232
|
+
|
|
233
|
+
Пример::
|
|
234
|
+
|
|
235
|
+
fee = client.get_payout_fee_info()
|
|
236
|
+
print(fee.text)
|
|
237
|
+
# Стоимость вывода денег на карты Т-Банка — 5%
|
|
238
|
+
# Стоимость вывода денег на карты других банков — 7%*
|
|
239
|
+
"""
|
|
240
|
+
data = self._get("/payout/fee/info")
|
|
241
|
+
return PayoutFeeInfo.from_dict(data.get("data", {}))
|
|
242
|
+
|
|
243
|
+
def get_accumulation_summary(self) -> AccumulationSummary:
|
|
244
|
+
"""
|
|
245
|
+
Получить сводку по накопленным средствам (баланс к выводу).
|
|
246
|
+
|
|
247
|
+
:return: :class:`AccumulationSummary`
|
|
248
|
+
|
|
249
|
+
Пример::
|
|
250
|
+
|
|
251
|
+
s = client.get_accumulation_summary()
|
|
252
|
+
print(f"Накоплено: {s.accumulated_amount}₽")
|
|
253
|
+
print(f"Комиссия: {s.commission_percent}%")
|
|
254
|
+
print(f"Следующая выплата: {s.next_payout_date or 'не запланирована'}")
|
|
255
|
+
"""
|
|
256
|
+
data = self._get("/accumulations/summary")
|
|
257
|
+
return AccumulationSummary.from_dict(data.get("data", {}))
|
|
258
|
+
|
|
259
|
+
def get_payout_method(self) -> str:
|
|
260
|
+
"""
|
|
261
|
+
Получить текущий метод выплат.
|
|
262
|
+
|
|
263
|
+
:return: ``"Instant"`` или ``"Accumulation"``
|
|
264
|
+
"""
|
|
265
|
+
return self.get_me().payout_method
|
|
266
|
+
|
|
267
|
+
def set_payout_method(self, method: str = "Instant") -> bool:
|
|
268
|
+
"""
|
|
269
|
+
Установить метод выплат.
|
|
270
|
+
|
|
271
|
+
:param method: ``"Instant"`` (мгновенно) или ``"Accumulation"`` (накопительно)
|
|
272
|
+
:return: ``True`` если успешно
|
|
273
|
+
"""
|
|
274
|
+
data = self._post("/receivers/payout-method", json={"payoutMethod": method})
|
|
275
|
+
return data.get("succeed", False)
|
|
276
|
+
|
|
277
|
+
# ------------------------------------------------------------------
|
|
278
|
+
# Внутренние HTTP-методы
|
|
177
279
|
# ------------------------------------------------------------------
|
|
178
280
|
|
|
179
281
|
def _get(self, path: str, params: Optional[dict] = None) -> dict:
|
|
@@ -187,6 +289,28 @@ class CloudTipsClient:
|
|
|
187
289
|
_raise_for_status(response)
|
|
188
290
|
return response.json()
|
|
189
291
|
|
|
292
|
+
def _post(self, path: str, json: Optional[dict] = None) -> dict:
|
|
293
|
+
headers = {**HEADERS_BASE, **self._auth.headers()}
|
|
294
|
+
response = self._session.post(
|
|
295
|
+
self._base_url + path,
|
|
296
|
+
headers=headers,
|
|
297
|
+
json=json,
|
|
298
|
+
timeout=15,
|
|
299
|
+
)
|
|
300
|
+
_raise_for_status(response)
|
|
301
|
+
return response.json()
|
|
302
|
+
|
|
303
|
+
def _delete(self, path: str, json: Optional[dict] = None) -> dict:
|
|
304
|
+
headers = {**HEADERS_BASE, **self._auth.headers()}
|
|
305
|
+
response = self._session.delete(
|
|
306
|
+
self._base_url + path,
|
|
307
|
+
headers=headers,
|
|
308
|
+
json=json,
|
|
309
|
+
timeout=15,
|
|
310
|
+
)
|
|
311
|
+
_raise_for_status(response)
|
|
312
|
+
return response.json()
|
|
313
|
+
|
|
190
314
|
|
|
191
315
|
class CloudTipsAPIError(Exception):
|
|
192
316
|
"""Ошибка запроса к CloudTips API."""
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Donation:
|
|
8
|
+
transaction_id: int
|
|
9
|
+
name: str
|
|
10
|
+
amount: int # рубли
|
|
11
|
+
tg_id: int
|
|
12
|
+
comment: str
|
|
13
|
+
date: datetime
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_dict(cls, data: dict) -> "Donation":
|
|
17
|
+
raw_date = data["date"]
|
|
18
|
+
try:
|
|
19
|
+
dt = datetime.fromisoformat(raw_date)
|
|
20
|
+
except ValueError:
|
|
21
|
+
dt = datetime.fromisoformat(raw_date[:19])
|
|
22
|
+
|
|
23
|
+
return cls(
|
|
24
|
+
transaction_id=data["transaction_id"],
|
|
25
|
+
name=data.get("name", ""),
|
|
26
|
+
amount=data["amount"],
|
|
27
|
+
tg_id=data.get("tg_id", 0),
|
|
28
|
+
comment=data.get("comment", ""),
|
|
29
|
+
date=dt,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
comment_part = f' — "{self.comment}"' if self.comment else ""
|
|
34
|
+
return (
|
|
35
|
+
f"[{self.date.strftime('%Y-%m-%d %H:%M')}] "
|
|
36
|
+
f"{self.name} → {self.amount}₽{comment_part}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Card:
|
|
42
|
+
token: str # токен для удаления и операций
|
|
43
|
+
first_six: str # первые 6 цифр (BIN)
|
|
44
|
+
last_four: str # последние 4 цифры
|
|
45
|
+
card_type: str # MIR / VISA / MASTERCARD
|
|
46
|
+
expiration_date: str # MM/YY
|
|
47
|
+
issuer_code: str # название банка
|
|
48
|
+
is_default: bool # карта по умолчанию для выплат
|
|
49
|
+
commission_hint: str # текст подсказки о комиссии
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_dict(cls, data: dict) -> "Card":
|
|
53
|
+
return cls(
|
|
54
|
+
token=data["token"],
|
|
55
|
+
first_six=data.get("firstSix", ""),
|
|
56
|
+
last_four=data.get("lastFour", ""),
|
|
57
|
+
card_type=data.get("cardType", ""),
|
|
58
|
+
expiration_date=data.get("cardExpirationDate", ""),
|
|
59
|
+
issuer_code=data.get("issuerCode", ""),
|
|
60
|
+
is_default=data.get("isDefault", False),
|
|
61
|
+
commission_hint=data.get("commissionHint", ""),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
default = " [по умолчанию]" if self.is_default else ""
|
|
66
|
+
return (
|
|
67
|
+
f"{self.card_type} *{self.last_four} "
|
|
68
|
+
f"({self.issuer_code}, до {self.expiration_date}){default}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class PayoutFeeInfo:
|
|
74
|
+
text: str # текст с описанием комиссий
|
|
75
|
+
downgrade_condition: str
|
|
76
|
+
tinkoff_commission_hint: str
|
|
77
|
+
instant_payout_commission_text: str
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_dict(cls, data: dict) -> "PayoutFeeInfo":
|
|
81
|
+
return cls(
|
|
82
|
+
text=data.get("text", ""),
|
|
83
|
+
downgrade_condition=data.get("downGradeCondition", ""),
|
|
84
|
+
tinkoff_commission_hint=data.get("tinkoffCommissionHint", ""),
|
|
85
|
+
instant_payout_commission_text=data.get("instantPayoutCommissionText", ""),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class AccumulationSummary:
|
|
91
|
+
accumulated_amount: float # накоплено (ещё не выведено)
|
|
92
|
+
amount_to_deposit: float # к зачислению
|
|
93
|
+
commission: float # сумма комиссии
|
|
94
|
+
commission_percent: float # процент комиссии
|
|
95
|
+
next_payout_date: Optional[str] # дата следующей выплаты (None если не запланирована)
|
|
96
|
+
commission_hint: str # текст подсказки
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_dict(cls, data: dict) -> "AccumulationSummary":
|
|
100
|
+
return cls(
|
|
101
|
+
accumulated_amount=data.get("accumulatedAmount", 0.0),
|
|
102
|
+
amount_to_deposit=data.get("amountToDeposit", 0.0),
|
|
103
|
+
commission=data.get("commission", 0.0),
|
|
104
|
+
commission_percent=data.get("commissionPercent", 0.0),
|
|
105
|
+
next_payout_date=data.get("nextPayoutDate"),
|
|
106
|
+
commission_hint=data.get("commissionHint", ""),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class ReceiverProfile:
|
|
112
|
+
user_id: str
|
|
113
|
+
full_name: str
|
|
114
|
+
phone_number: str
|
|
115
|
+
photo_url: str
|
|
116
|
+
payout_method: str # "Instant" или "Accumulation"
|
|
117
|
+
instant_payout_enabled: bool
|
|
118
|
+
is_premium: bool
|
|
119
|
+
onboarding_passed: bool
|
|
120
|
+
gender: str
|
|
121
|
+
work_place: Optional[str]
|
|
122
|
+
work_position: Optional[str]
|
|
123
|
+
birthday: Optional[str]
|
|
124
|
+
created_date: str
|
|
125
|
+
available_amount_min: float
|
|
126
|
+
available_amount_max: float
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def from_dict(cls, data: dict) -> "ReceiverProfile":
|
|
130
|
+
available = data.get("availableAmount") or {}
|
|
131
|
+
return cls(
|
|
132
|
+
user_id=data.get("userId", ""),
|
|
133
|
+
full_name=data.get("fullName", ""),
|
|
134
|
+
phone_number=data.get("phoneNumber", ""),
|
|
135
|
+
photo_url=data.get("photoUrl", ""),
|
|
136
|
+
payout_method=data.get("payoutMethod", "Instant"),
|
|
137
|
+
instant_payout_enabled=data.get("instantPayoutEnabled", False),
|
|
138
|
+
is_premium=data.get("isPremium", False),
|
|
139
|
+
onboarding_passed=data.get("onboardingPassed", False),
|
|
140
|
+
gender=data.get("gender", "NotSpecified"),
|
|
141
|
+
work_place=data.get("workPlace"),
|
|
142
|
+
work_position=data.get("workPosition"),
|
|
143
|
+
birthday=data.get("birthday"),
|
|
144
|
+
created_date=data.get("createdDate", ""),
|
|
145
|
+
available_amount_min=available.get("minimal", 0.0),
|
|
146
|
+
available_amount_max=available.get("maximal", 0.0),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def __str__(self) -> str:
|
|
150
|
+
return (
|
|
151
|
+
f"{self.full_name} ({self.phone_number})\n"
|
|
152
|
+
f"Метод выплат: {self.payout_method} | "
|
|
153
|
+
f"Премиум: {'да' if self.is_premium else 'нет'} | "
|
|
154
|
+
f"Лимиты: {self.available_amount_min}₽ — {self.available_amount_max}₽"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
"""Новые токены, которые библиотека передаёт в on_token_refresh."""
|
|
159
|
+
access_token: str
|
|
160
|
+
refresh_token: str
|
|
161
|
+
expires_at: float # unix timestamp
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cloudtips"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Неофициальная Python-библиотека для CloudTips (получение донатов, поллинг, обновление токенов)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
@dataclass
|
|
7
|
-
class Donation:
|
|
8
|
-
transaction_id: int
|
|
9
|
-
name: str
|
|
10
|
-
amount: int # рубли
|
|
11
|
-
tg_id: int
|
|
12
|
-
comment: str
|
|
13
|
-
date: datetime
|
|
14
|
-
|
|
15
|
-
@classmethod
|
|
16
|
-
def from_dict(cls, data: dict) -> "Donation":
|
|
17
|
-
raw_date = data["date"]
|
|
18
|
-
# Убираем offset вида +03:00 → datetime aware через fromisoformat (3.11+)
|
|
19
|
-
# или ручной парсинг для 3.9/3.10
|
|
20
|
-
try:
|
|
21
|
-
dt = datetime.fromisoformat(raw_date)
|
|
22
|
-
except ValueError:
|
|
23
|
-
# fallback: обрезаем offset
|
|
24
|
-
dt = datetime.fromisoformat(raw_date[:19])
|
|
25
|
-
|
|
26
|
-
return cls(
|
|
27
|
-
transaction_id=data["transaction_id"],
|
|
28
|
-
name=data.get("name", ""),
|
|
29
|
-
amount=data["amount"],
|
|
30
|
-
tg_id=data.get("tg_id", 0),
|
|
31
|
-
comment=data.get("comment", ""),
|
|
32
|
-
date=dt,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
def __str__(self) -> str:
|
|
36
|
-
comment_part = f' — "{self.comment}"' if self.comment else ""
|
|
37
|
-
return (
|
|
38
|
-
f"[{self.date.strftime('%Y-%m-%d %H:%M')}] "
|
|
39
|
-
f"{self.name} → {self.amount}₽{comment_part}"
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@dataclass
|
|
44
|
-
class TokenData:
|
|
45
|
-
"""Новые токены, которые библиотека передаёт в on_token_refresh."""
|
|
46
|
-
access_token: str
|
|
47
|
-
refresh_token: str
|
|
48
|
-
expires_at: float # unix timestamp
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|