cloudtips 0.3.1__tar.gz → 0.4.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.
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: cloudtips
3
+ Version: 0.4.0
4
+ Summary: Неофициальная асинхронная Python-библиотека для CloudTips (получение донатов, поллинг, обновление токенов)
5
+ Project-URL: Repository, https://github.com/IRRatium/cloudtips-api
6
+ Project-URL: Issues, https://github.com/IRRatium/cloudtips-api/issues
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: api,async,asyncio,cloudtips,donations,payments,tips
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Internet :: WWW/HTTP
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: aiohttp>=3.9
21
+ Description-Content-Type: text/markdown
22
+
23
+ # CloudtipsAPI
24
+
25
+ Неофициальная асинхронная Python-библиотека для [CloudTips](https://cloudtips.ru) — получение донатов, поллинг новых поступлений и автоматическое обновление токенов.
26
+
27
+ ## Установка
28
+
29
+ ```bash
30
+ pip install cloudtips
31
+ ```
32
+
33
+ ## Быстрый старт
34
+
35
+ ```python
36
+ import asyncio
37
+ import json
38
+ from cloudtips import CloudTipsAuth, CloudTipsClient, TokenData
39
+
40
+ # Загружаем токены из файла
41
+ with open("donate.json") as f:
42
+ config = json.load(f)
43
+
44
+ # Колбэк вызывается каждый раз при обновлении токенов.
45
+ # Поддерживаются как async, так и обычные функции.
46
+ # Refresh-токен одноразовый — обязательно сохраняйте новые!
47
+ async def on_token_refresh(token_data: TokenData):
48
+ config["cloudtips_token"] = token_data.access_token
49
+ config["cloudtips_refresh_token"] = token_data.refresh_token
50
+ config["cloudtips_expires_at"] = token_data.expires_at
51
+ with open("donate.json", "w") as f:
52
+ json.dump(config, f, ensure_ascii=False, indent=2)
53
+ print("Токены обновлены и сохранены.")
54
+
55
+ auth = CloudTipsAuth(
56
+ token=config["cloudtips_token"],
57
+ refresh_token=config["cloudtips_refresh_token"],
58
+ expires_at=config["cloudtips_expires_at"],
59
+ on_token_refresh=on_token_refresh,
60
+ )
61
+
62
+ async def main():
63
+ async with CloudTipsClient(auth) as client:
64
+ donations = await client.get_all_donations()
65
+ for d in donations:
66
+ print(d)
67
+
68
+ asyncio.run(main())
69
+ ```
70
+
71
+ ## Получение донатов
72
+
73
+ ```python
74
+ async with CloudTipsClient(auth) as client:
75
+ # Все донаты за последние 24 часа
76
+ donations = await client.get_all_donations()
77
+ for d in donations:
78
+ print(d) # [2026-04-10 20:44] евгения → 50₽ — "оч крутой сервис"
79
+
80
+ # Только за конкретный период
81
+ from datetime import datetime, timedelta, timezone
82
+
83
+ yesterday = datetime.now(tz=timezone.utc) - timedelta(days=1)
84
+ recent = await client.get_donations(since=yesterday)
85
+ ```
86
+
87
+ ## Поллинг новых донатов
88
+
89
+ ### Вариант 1 — async-генератор
90
+
91
+ ```python
92
+ async with CloudTipsClient(auth) as client:
93
+ print("Слушаем новые донаты...")
94
+ async for donation in client.poll(interval=30):
95
+ print(f"💰 {donation.name} задонатил {donation.amount}₽")
96
+ if donation.comment:
97
+ print(f" Комментарий: {donation.comment}")
98
+ ```
99
+
100
+ ### Вариант 2 — async-колбэк
101
+
102
+ ```python
103
+ async def handle_donation(donation):
104
+ print(f"Новый донат от {donation.name}: {donation.amount}₽")
105
+ # await bot.send_message(...)
106
+
107
+ async with CloudTipsClient(auth) as client:
108
+ await client.poll(interval=15, callback=handle_donation)
109
+ ```
110
+
111
+ ### Вариант 3 — в фоновой задаче asyncio
112
+
113
+ ```python
114
+ async def poll_task(client):
115
+ async for donation in client.poll(interval=30):
116
+ print(f"Новый донат: {donation}")
117
+
118
+ async def main():
119
+ async with CloudTipsClient(auth) as client:
120
+ task = asyncio.create_task(poll_task(client))
121
+ # Основная логика...
122
+ await task
123
+
124
+ asyncio.run(main())
125
+ ```
126
+
127
+ ## Профиль, карты и баланс
128
+
129
+ ```python
130
+ async with CloudTipsClient(auth) as client:
131
+ # Профиль пользователя
132
+ me = await client.get_me()
133
+ print(me.full_name) # IRRing
134
+ print(me.payout_method) # Accumulation
135
+
136
+ # Привязанные карты
137
+ for card in await client.get_cards():
138
+ print(card) # MIR *3742 (T-BANK, до 08/34) [по умолчанию]
139
+ print(card.token) # tk_89e6b3c6827afd4e9ccc36db2d22f
140
+
141
+ # Баланс к выводу
142
+ s = await client.get_accumulation_summary()
143
+ print(f"Накоплено: {s.accumulated_amount}₽")
144
+ print(f"Комиссия: {s.commission_percent}%")
145
+ print(f"Следующая выплата: {s.next_payout_date or 'не запланирована'}")
146
+
147
+ # Информация о комиссиях
148
+ fee = await client.get_payout_fee_info()
149
+ print(fee.text)
150
+
151
+ # Смена метода выплат
152
+ await client.set_payout_method("Instant") # мгновенно
153
+ await client.set_payout_method("Accumulation") # накопительно
154
+
155
+ # Удаление карты
156
+ for card in await client.get_cards():
157
+ await client.delete_card(card.token)
158
+ ```
159
+
160
+ ## Структура `Donation`
161
+
162
+ ```python
163
+ @dataclass
164
+ class Donation:
165
+ transaction_id: int # уникальный ID транзакции
166
+ name: str # имя донатера
167
+ amount: int # сумма в рублях
168
+ tg_id: int # Telegram ID
169
+ comment: str # комментарий (может быть пустым)
170
+ date: datetime # дата и время
171
+
172
+ str(donation)
173
+ # "[2026-04-10 23:04] Каспер → 200₽ — "спасибо за отличный сервис)""
174
+ ```
175
+
176
+ ## Обработка ошибок
177
+
178
+ ```python
179
+ from cloudtips import CloudTipsAuthError, CloudTipsAPIError
180
+
181
+ try:
182
+ async with CloudTipsClient(auth) as client:
183
+ donations = await client.get_donations()
184
+ except CloudTipsAuthError as e:
185
+ print(f"Проблема с аутентификацией: {e}")
186
+ except CloudTipsAPIError as e:
187
+ print(f"Ошибка API (HTTP {e.status_code}): {e.detail}")
188
+ ```
189
+
190
+ ## Примечания
191
+
192
+ - **Refresh-токен одноразовый.** После каждого обновления старый токен становится недействительным. Всегда передавайте `on_token_refresh` и сохраняйте новые токены.
193
+ - `on_token_refresh` поддерживает как обычные (`def`), так и async-функции (`async def`).
194
+ - Библиотека автоматически обновляет токен за 2 минуты до истечения.
195
+ - Поллинг отслеживает уже виденные `transaction_id`, поэтому дублей не будет.
196
+ - Клиент — контекстный менеджер (`async with`), это рекомендуемый способ использования.
197
+
198
+ ## Лицензия
199
+
200
+ MIT
@@ -0,0 +1,178 @@
1
+ # CloudtipsAPI
2
+
3
+ Неофициальная асинхронная Python-библиотека для [CloudTips](https://cloudtips.ru) — получение донатов, поллинг новых поступлений и автоматическое обновление токенов.
4
+
5
+ ## Установка
6
+
7
+ ```bash
8
+ pip install cloudtips
9
+ ```
10
+
11
+ ## Быстрый старт
12
+
13
+ ```python
14
+ import asyncio
15
+ import json
16
+ from cloudtips import CloudTipsAuth, CloudTipsClient, TokenData
17
+
18
+ # Загружаем токены из файла
19
+ with open("donate.json") as f:
20
+ config = json.load(f)
21
+
22
+ # Колбэк вызывается каждый раз при обновлении токенов.
23
+ # Поддерживаются как async, так и обычные функции.
24
+ # Refresh-токен одноразовый — обязательно сохраняйте новые!
25
+ async def on_token_refresh(token_data: TokenData):
26
+ config["cloudtips_token"] = token_data.access_token
27
+ config["cloudtips_refresh_token"] = token_data.refresh_token
28
+ config["cloudtips_expires_at"] = token_data.expires_at
29
+ with open("donate.json", "w") as f:
30
+ json.dump(config, f, ensure_ascii=False, indent=2)
31
+ print("Токены обновлены и сохранены.")
32
+
33
+ auth = CloudTipsAuth(
34
+ token=config["cloudtips_token"],
35
+ refresh_token=config["cloudtips_refresh_token"],
36
+ expires_at=config["cloudtips_expires_at"],
37
+ on_token_refresh=on_token_refresh,
38
+ )
39
+
40
+ async def main():
41
+ async with CloudTipsClient(auth) as client:
42
+ donations = await client.get_all_donations()
43
+ for d in donations:
44
+ print(d)
45
+
46
+ asyncio.run(main())
47
+ ```
48
+
49
+ ## Получение донатов
50
+
51
+ ```python
52
+ async with CloudTipsClient(auth) as client:
53
+ # Все донаты за последние 24 часа
54
+ donations = await client.get_all_donations()
55
+ for d in donations:
56
+ print(d) # [2026-04-10 20:44] евгения → 50₽ — "оч крутой сервис"
57
+
58
+ # Только за конкретный период
59
+ from datetime import datetime, timedelta, timezone
60
+
61
+ yesterday = datetime.now(tz=timezone.utc) - timedelta(days=1)
62
+ recent = await client.get_donations(since=yesterday)
63
+ ```
64
+
65
+ ## Поллинг новых донатов
66
+
67
+ ### Вариант 1 — async-генератор
68
+
69
+ ```python
70
+ async with CloudTipsClient(auth) as client:
71
+ print("Слушаем новые донаты...")
72
+ async for donation in client.poll(interval=30):
73
+ print(f"💰 {donation.name} задонатил {donation.amount}₽")
74
+ if donation.comment:
75
+ print(f" Комментарий: {donation.comment}")
76
+ ```
77
+
78
+ ### Вариант 2 — async-колбэк
79
+
80
+ ```python
81
+ async def handle_donation(donation):
82
+ print(f"Новый донат от {donation.name}: {donation.amount}₽")
83
+ # await bot.send_message(...)
84
+
85
+ async with CloudTipsClient(auth) as client:
86
+ await client.poll(interval=15, callback=handle_donation)
87
+ ```
88
+
89
+ ### Вариант 3 — в фоновой задаче asyncio
90
+
91
+ ```python
92
+ async def poll_task(client):
93
+ async for donation in client.poll(interval=30):
94
+ print(f"Новый донат: {donation}")
95
+
96
+ async def main():
97
+ async with CloudTipsClient(auth) as client:
98
+ task = asyncio.create_task(poll_task(client))
99
+ # Основная логика...
100
+ await task
101
+
102
+ asyncio.run(main())
103
+ ```
104
+
105
+ ## Профиль, карты и баланс
106
+
107
+ ```python
108
+ async with CloudTipsClient(auth) as client:
109
+ # Профиль пользователя
110
+ me = await client.get_me()
111
+ print(me.full_name) # IRRing
112
+ print(me.payout_method) # Accumulation
113
+
114
+ # Привязанные карты
115
+ for card in await client.get_cards():
116
+ print(card) # MIR *3742 (T-BANK, до 08/34) [по умолчанию]
117
+ print(card.token) # tk_89e6b3c6827afd4e9ccc36db2d22f
118
+
119
+ # Баланс к выводу
120
+ s = await client.get_accumulation_summary()
121
+ print(f"Накоплено: {s.accumulated_amount}₽")
122
+ print(f"Комиссия: {s.commission_percent}%")
123
+ print(f"Следующая выплата: {s.next_payout_date or 'не запланирована'}")
124
+
125
+ # Информация о комиссиях
126
+ fee = await client.get_payout_fee_info()
127
+ print(fee.text)
128
+
129
+ # Смена метода выплат
130
+ await client.set_payout_method("Instant") # мгновенно
131
+ await client.set_payout_method("Accumulation") # накопительно
132
+
133
+ # Удаление карты
134
+ for card in await client.get_cards():
135
+ await client.delete_card(card.token)
136
+ ```
137
+
138
+ ## Структура `Donation`
139
+
140
+ ```python
141
+ @dataclass
142
+ class Donation:
143
+ transaction_id: int # уникальный ID транзакции
144
+ name: str # имя донатера
145
+ amount: int # сумма в рублях
146
+ tg_id: int # Telegram ID
147
+ comment: str # комментарий (может быть пустым)
148
+ date: datetime # дата и время
149
+
150
+ str(donation)
151
+ # "[2026-04-10 23:04] Каспер → 200₽ — "спасибо за отличный сервис)""
152
+ ```
153
+
154
+ ## Обработка ошибок
155
+
156
+ ```python
157
+ from cloudtips import CloudTipsAuthError, CloudTipsAPIError
158
+
159
+ try:
160
+ async with CloudTipsClient(auth) as client:
161
+ donations = await client.get_donations()
162
+ except CloudTipsAuthError as e:
163
+ print(f"Проблема с аутентификацией: {e}")
164
+ except CloudTipsAPIError as e:
165
+ print(f"Ошибка API (HTTP {e.status_code}): {e.detail}")
166
+ ```
167
+
168
+ ## Примечания
169
+
170
+ - **Refresh-токен одноразовый.** После каждого обновления старый токен становится недействительным. Всегда передавайте `on_token_refresh` и сохраняйте новые токены.
171
+ - `on_token_refresh` поддерживает как обычные (`def`), так и async-функции (`async def`).
172
+ - Библиотека автоматически обновляет токен за 2 минуты до истечения.
173
+ - Поллинг отслеживает уже виденные `transaction_id`, поэтому дублей не будет.
174
+ - Клиент — контекстный менеджер (`async with`), это рекомендуемый способ использования.
175
+
176
+ ## Лицензия
177
+
178
+ MIT
@@ -1,12 +1,12 @@
1
1
  """
2
- cloudtips — неофициальная Python-библиотека для CloudTips.
2
+ cloudtips — неофициальная асинхронная Python-библиотека для CloudTips.
3
3
  """
4
4
 
5
5
  from .auth import CloudTipsAuth, CloudTipsAuthError
6
6
  from .client import CloudTipsAPIError, CloudTipsClient
7
7
  from .models import AccumulationSummary, Card, Donation, PayoutFeeInfo, ReceiverProfile, TokenData
8
8
 
9
- __version__ = "0.3.1"
9
+ __version__ = "0.4.0"
10
10
  __all__ = [
11
11
  "CloudTipsAuth",
12
12
  "CloudTipsAuthError",
@@ -1,7 +1,10 @@
1
+ """
2
+ Аутентификация CloudTips — управление access/refresh токенами.
3
+ """
1
4
  import time
2
5
  from typing import Callable, Optional
3
6
 
4
- import requests
7
+ import aiohttp
5
8
 
6
9
  from .models import TokenData
7
10
 
@@ -14,17 +17,17 @@ class CloudTipsAuth:
14
17
  """
15
18
  Управляет access/refresh токенами CloudTips.
16
19
 
17
- Параметр `on_token_refresh`это колбэк, который вызывается каждый раз
18
- после успешного обновления токенов. Используйте его, чтобы сохранить
19
- новые токены в файл/БД/переменные окружения — refresh-токен одноразовый!
20
+ Параметр ``on_token_refresh`` — колбэк, вызываемый после каждого
21
+ успешного обновления токенов. Refresh-токен одноразовый, поэтому
22
+ обязательно сохраняйте новые значения.
20
23
 
21
24
  Пример::
22
25
 
23
- def save_tokens(token_data: TokenData):
26
+ async def save_tokens(token_data: TokenData):
24
27
  config["cloudtips_token"] = token_data.access_token
25
28
  config["cloudtips_refresh_token"] = token_data.refresh_token
26
29
  config["cloudtips_expires_at"] = token_data.expires_at
27
- save_config(config)
30
+ await write_config(config)
28
31
 
29
32
  auth = CloudTipsAuth(
30
33
  token="...",
@@ -50,31 +53,31 @@ class CloudTipsAuth:
50
53
  # Public
51
54
  # ------------------------------------------------------------------
52
55
 
53
- @property
54
- def token(self) -> str:
56
+ async def get_token(self) -> str:
55
57
  """Возвращает актуальный access-токен, автоматически обновляя при необходимости."""
56
58
  if self._is_expired():
57
- self.refresh()
59
+ await self.refresh()
58
60
  return self._token
59
61
 
60
62
  @property
61
63
  def expires_at(self) -> float:
62
64
  return self._expires_at
63
65
 
64
- def refresh(self) -> TokenData:
66
+ async def refresh(self) -> TokenData:
65
67
  """Принудительно обновляет токены через CloudTips Identity Server."""
66
- response = requests.post(
67
- _TOKEN_URL,
68
- data={
69
- "grant_type": "refresh_token",
70
- "refresh_token": self._refresh_token,
71
- "client_id": _CLIENT_ID,
72
- },
73
- timeout=10,
74
- )
75
- _raise_for_status(response)
68
+ async with aiohttp.ClientSession() as session:
69
+ async with session.post(
70
+ _TOKEN_URL,
71
+ data={
72
+ "grant_type": "refresh_token",
73
+ "refresh_token": self._refresh_token,
74
+ "client_id": _CLIENT_ID,
75
+ },
76
+ timeout=aiohttp.ClientTimeout(total=10),
77
+ ) as response:
78
+ await _raise_for_status(response)
79
+ data = await response.json()
76
80
 
77
- data = response.json()
78
81
  token_data = TokenData(
79
82
  access_token=data["access_token"],
80
83
  refresh_token=data["refresh_token"],
@@ -86,15 +89,18 @@ class CloudTipsAuth:
86
89
  self._expires_at = token_data.expires_at
87
90
 
88
91
  if self._on_token_refresh:
89
- self._on_token_refresh(token_data)
92
+ result = self._on_token_refresh(token_data)
93
+ # поддержка как async, так и обычных колбэков
94
+ if hasattr(result, "__await__"):
95
+ await result
90
96
 
91
97
  return token_data
92
98
 
93
- def headers(self) -> dict:
99
+ async def headers(self) -> dict:
94
100
  """Готовые Authorization-заголовки для запросов."""
95
101
  return {
96
- "Authorization": f"Bearer {self.token}",
97
- "Content-Type": "application/json",
102
+ "Authorization": f"Bearer {await self.get_token()}",
103
+ "Content-Type": "application/json",
98
104
  }
99
105
 
100
106
  # ------------------------------------------------------------------
@@ -105,17 +111,15 @@ class CloudTipsAuth:
105
111
  return time.time() >= self._expires_at - _EXPIRE_BUFFER
106
112
 
107
113
 
108
- def _raise_for_status(response: requests.Response) -> None:
109
- try:
110
- response.raise_for_status()
111
- except requests.HTTPError as e:
114
+ async def _raise_for_status(response: aiohttp.ClientResponse) -> None:
115
+ if not response.ok:
112
116
  try:
113
- detail = response.json()
117
+ detail = await response.json()
114
118
  except Exception:
115
- detail = response.text
119
+ detail = await response.text()
116
120
  raise CloudTipsAuthError(
117
- f"Ошибка при обновлении токена: {response.status_code} — {detail}"
118
- ) from e
121
+ f"Ошибка при обновлении токена: {response.status} — {detail}"
122
+ )
119
123
 
120
124
 
121
125
  class CloudTipsAuthError(Exception):
@@ -1,34 +1,50 @@
1
1
  """
2
- Клиент CloudTips API.
2
+ Асинхронный клиент CloudTips API.
3
3
  """
4
- import time
4
+ import asyncio
5
5
  from datetime import datetime, timezone, timedelta
6
- from typing import Callable, Iterator, List, Optional
6
+ from typing import AsyncIterator, Callable, List, Optional
7
7
 
8
- import requests
8
+ import aiohttp
9
9
 
10
10
  from .auth import CloudTipsAuth
11
- from .models import Donation, Card, PayoutFeeInfo, AccumulationSummary, ReceiverProfile
11
+ from .models import AccumulationSummary, Card, Donation, PayoutFeeInfo, ReceiverProfile
12
12
 
13
13
  _BASE_URL = "https://api.cloudtips.ru/api"
14
14
  _MSK = timezone(timedelta(hours=3))
15
15
 
16
- HEADERS_BASE = {
17
- "Accept": "application/json, text/plain, */*",
16
+ _HEADERS_BASE = {
17
+ "Accept": "application/json, text/plain, */*",
18
18
  "Accept-Language": "ru-RU,ru;q=0.9",
19
- "Origin": "https://lk.cloudtips.ru",
20
- "Referer": "https://lk.cloudtips.ru/",
21
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
19
+ "Origin": "https://lk.cloudtips.ru",
20
+ "Referer": "https://lk.cloudtips.ru/",
21
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
22
22
  }
23
23
 
24
24
 
25
25
  class CloudTipsClient:
26
26
  """
27
- Основной клиент для работы с CloudTips API.
27
+ Асинхронный клиент для работы с CloudTips API.
28
+
29
+ Используйте как контекстный менеджер, чтобы сессия aiohttp
30
+ корректно открывалась и закрывалась::
31
+
32
+ async with CloudTipsClient(auth) as client:
33
+ donations = await client.get_all_donations()
34
+
35
+ Или управляйте сессией вручную::
36
+
37
+ client = CloudTipsClient(auth)
38
+ await client.open()
39
+ try:
40
+ donations = await client.get_all_donations()
41
+ finally:
42
+ await client.close()
28
43
 
29
44
  Пример быстрого старта::
30
45
 
31
- from cloudtips import CloudTipsClient, CloudTipsAuth
46
+ import asyncio
47
+ from cloudtips import CloudTipsAuth, CloudTipsClient
32
48
 
33
49
  auth = CloudTipsAuth(
34
50
  token="...",
@@ -36,23 +52,48 @@ class CloudTipsClient:
36
52
  expires_at=1776099728.0,
37
53
  on_token_refresh=lambda td: print("Новый refresh:", td.refresh_token),
38
54
  )
39
- client = CloudTipsClient(auth)
40
55
 
41
- donations = client.get_all_donations()
42
- cards = client.get_cards()
43
- summary = client.get_accumulation_summary()
56
+ async def main():
57
+ async with CloudTipsClient(auth) as client:
58
+ donations = await client.get_all_donations()
59
+ cards = await client.get_cards()
60
+ summary = await client.get_accumulation_summary()
61
+
62
+ asyncio.run(main())
44
63
  """
45
64
 
46
65
  def __init__(self, auth: CloudTipsAuth, base_url: str = _BASE_URL) -> None:
47
66
  self._auth = auth
48
67
  self._base_url = base_url.rstrip("/")
49
- self._session = requests.Session()
68
+ self._session: Optional[aiohttp.ClientSession] = None
69
+
70
+ # ------------------------------------------------------------------
71
+ # Управление сессией
72
+ # ------------------------------------------------------------------
73
+
74
+ async def open(self) -> None:
75
+ """Открыть aiohttp-сессию."""
76
+ if self._session is None or self._session.closed:
77
+ self._session = aiohttp.ClientSession()
78
+
79
+ async def close(self) -> None:
80
+ """Закрыть aiohttp-сессию."""
81
+ if self._session and not self._session.closed:
82
+ await self._session.close()
83
+ self._session = None
84
+
85
+ async def __aenter__(self) -> "CloudTipsClient":
86
+ await self.open()
87
+ return self
88
+
89
+ async def __aexit__(self, *_) -> None:
90
+ await self.close()
50
91
 
51
92
  # ------------------------------------------------------------------
52
93
  # Донаты
53
94
  # ------------------------------------------------------------------
54
95
 
55
- def get_donations(
96
+ async def get_donations(
56
97
  self,
57
98
  since: Optional[datetime] = None,
58
99
  until: Optional[datetime] = None,
@@ -72,7 +113,7 @@ class CloudTipsClient:
72
113
  date_from = _ensure_tz(since or (now - timedelta(hours=24)))
73
114
  date_to = _ensure_tz(until or now)
74
115
 
75
- data = self._get("/timeline", params={
116
+ data = await self._get("/timeline", params={
76
117
  "page": page,
77
118
  "limit": limit,
78
119
  "dateFrom": date_from.isoformat(),
@@ -94,7 +135,7 @@ class CloudTipsClient:
94
135
  }))
95
136
  return result
96
137
 
97
- def get_all_donations(
138
+ async def get_all_donations(
98
139
  self,
99
140
  since: Optional[datetime] = None,
100
141
  until: Optional[datetime] = None,
@@ -109,7 +150,7 @@ class CloudTipsClient:
109
150
  result: List[Donation] = []
110
151
  page = 1
111
152
  while True:
112
- batch = self.get_donations(since=since, until=until, limit=50, page=page)
153
+ batch = await self.get_donations(since=since, until=until, limit=50, page=page)
113
154
  if not batch:
114
155
  break
115
156
  result.extend(batch)
@@ -118,23 +159,26 @@ class CloudTipsClient:
118
159
  page += 1
119
160
  return sorted(result, key=lambda d: d.date)
120
161
 
121
- def poll(
162
+ async def poll(
122
163
  self,
123
164
  interval: int = 30,
124
165
  since: Optional[datetime] = None,
125
166
  callback: Optional[Callable[[Donation], None]] = None,
126
- ) -> Iterator[Donation]:
167
+ ) -> AsyncIterator[Donation]:
127
168
  """
128
- Генератор: бесконечный поллинг новых донатов.
169
+ Асинхронный генератор: бесконечный поллинг новых донатов.
129
170
 
130
- Использование как генератора::
171
+ Использование как async-генератора::
131
172
 
132
- for donation in client.poll(interval=15):
173
+ async for donation in client.poll(interval=15):
133
174
  print(f"Новый донат: {donation}")
134
175
 
135
- Или с колбэком (блокирующий режим)::
176
+ Или с async-колбэком (блокирующий режим)::
177
+
178
+ async def handle(donation):
179
+ await bot.send_message(chat_id, str(donation))
136
180
 
137
- client.poll(interval=15, callback=handle_donation)
181
+ await client.poll(interval=15, callback=handle)
138
182
 
139
183
  :param interval: пауза между запросами в секундах
140
184
  :param since: с какого момента начинать (по умолчанию — прямо сейчас)
@@ -143,14 +187,14 @@ class CloudTipsClient:
143
187
  last_seen_ids: set = set()
144
188
  cursor = _ensure_tz(since or datetime.now(_MSK))
145
189
 
146
- for d in self.get_all_donations(since=cursor):
190
+ for d in await self.get_all_donations(since=cursor):
147
191
  last_seen_ids.add(d.transaction_id)
148
192
 
149
193
  while True:
150
- time.sleep(interval)
194
+ await asyncio.sleep(interval)
151
195
 
152
196
  try:
153
- fresh = self.get_all_donations(since=cursor)
197
+ fresh = await self.get_all_donations(since=cursor)
154
198
  except Exception as exc:
155
199
  print(f"[cloudtips] Ошибка при поллинге: {exc}")
156
200
  continue
@@ -160,7 +204,9 @@ class CloudTipsClient:
160
204
  last_seen_ids.add(donation.transaction_id)
161
205
  cursor = max(cursor, donation.date)
162
206
  if callback:
163
- callback(donation)
207
+ result = callback(donation)
208
+ if hasattr(result, "__await__"):
209
+ await result
164
210
  else:
165
211
  yield donation
166
212
 
@@ -168,29 +214,26 @@ class CloudTipsClient:
168
214
  # Профиль
169
215
  # ------------------------------------------------------------------
170
216
 
171
- def get_me(self) -> ReceiverProfile:
217
+ async def get_me(self) -> ReceiverProfile:
172
218
  """
173
219
  Получить профиль текущего пользователя.
174
220
 
175
- Содержит имя, телефон, метод выплат, лимиты сумм и другие данные.
176
-
177
221
  :return: :class:`ReceiverProfile`
178
222
 
179
223
  Пример::
180
224
 
181
- me = client.get_me()
225
+ me = await client.get_me()
182
226
  print(me.full_name) # IRRing
183
227
  print(me.payout_method) # Accumulation
184
- print(me.available_amount_min, me.available_amount_max) # 49.0 3000.0
185
228
  """
186
- data = self._get("/receivers/me")
229
+ data = await self._get("/receivers/me")
187
230
  return ReceiverProfile.from_dict(data.get("data", {}))
188
231
 
189
232
  # ------------------------------------------------------------------
190
233
  # Карты
191
234
  # ------------------------------------------------------------------
192
235
 
193
- def get_cards(self) -> List[Card]:
236
+ async def get_cards(self) -> List[Card]:
194
237
  """
195
238
  Получить список привязанных карт.
196
239
 
@@ -198,49 +241,37 @@ class CloudTipsClient:
198
241
 
199
242
  Пример::
200
243
 
201
- for card in client.get_cards():
202
- print(card) # MIR *3742 (T-BANK (TINKOFF), до 08/34) [по умолчанию]
244
+ for card in await client.get_cards():
245
+ print(card) # MIR *3742 (T-BANK, до 08/34) [по умолчанию]
203
246
  print(card.token) # tk_89e6b3c6827afd4e9ccc36db2d22f
204
247
  """
205
- data = self._get("/cards")
248
+ data = await self._get("/cards")
206
249
  return [Card.from_dict(item) for item in data.get("data", [])]
207
250
 
208
- def delete_card(self, card_token: str) -> bool:
251
+ async def delete_card(self, card_token: str) -> bool:
209
252
  """
210
253
  Удалить привязанную карту.
211
254
 
212
255
  :param card_token: токен карты (``card.token``)
213
256
  :return: ``True`` если удаление прошло успешно
214
-
215
- Пример::
216
-
217
- for card in client.get_cards():
218
- client.delete_card(card.token)
219
257
  """
220
- data = self._delete("/cards", json={"cardToken": card_token})
258
+ data = await self._delete("/cards", json={"cardToken": card_token})
221
259
  return data.get("succeed", False)
222
260
 
223
261
  # ------------------------------------------------------------------
224
262
  # Выплаты и баланс
225
263
  # ------------------------------------------------------------------
226
264
 
227
- def get_payout_fee_info(self) -> PayoutFeeInfo:
265
+ async def get_payout_fee_info(self) -> PayoutFeeInfo:
228
266
  """
229
267
  Получить информацию о комиссиях при выводе средств.
230
268
 
231
269
  :return: :class:`PayoutFeeInfo`
232
-
233
- Пример::
234
-
235
- fee = client.get_payout_fee_info()
236
- print(fee.text)
237
- # Стоимость вывода денег на карты Т-Банка — 5%
238
- # Стоимость вывода денег на карты других банков — 7%*
239
270
  """
240
- data = self._get("/payout/fee/info")
271
+ data = await self._get("/payout/fee/info")
241
272
  return PayoutFeeInfo.from_dict(data.get("data", {}))
242
273
 
243
- def get_accumulation_summary(self) -> AccumulationSummary:
274
+ async def get_accumulation_summary(self) -> AccumulationSummary:
244
275
  """
245
276
  Получить сводку по накопленным средствам (баланс к выводу).
246
277
 
@@ -248,68 +279,78 @@ class CloudTipsClient:
248
279
 
249
280
  Пример::
250
281
 
251
- s = client.get_accumulation_summary()
282
+ s = await client.get_accumulation_summary()
252
283
  print(f"Накоплено: {s.accumulated_amount}₽")
253
284
  print(f"Комиссия: {s.commission_percent}%")
254
- print(f"Следующая выплата: {s.next_payout_date or 'не запланирована'}")
255
285
  """
256
- data = self._get("/accumulations/summary")
286
+ data = await self._get("/accumulations/summary")
257
287
  return AccumulationSummary.from_dict(data.get("data", {}))
258
288
 
259
- def get_payout_method(self) -> str:
289
+ async def get_payout_method(self) -> str:
260
290
  """
261
291
  Получить текущий метод выплат.
262
292
 
263
293
  :return: ``"Instant"`` или ``"Accumulation"``
264
294
  """
265
- return self.get_me().payout_method
295
+ return (await self.get_me()).payout_method
266
296
 
267
- def set_payout_method(self, method: str = "Instant") -> bool:
297
+ async def set_payout_method(self, method: str = "Instant") -> bool:
268
298
  """
269
299
  Установить метод выплат.
270
300
 
271
301
  :param method: ``"Instant"`` (мгновенно) или ``"Accumulation"`` (накопительно)
272
302
  :return: ``True`` если успешно
273
303
  """
274
- data = self._post("/receivers/payout-method", json={"payoutMethod": method})
304
+ data = await self._post("/receivers/payout-method", json={"payoutMethod": method})
275
305
  return data.get("succeed", False)
276
306
 
277
307
  # ------------------------------------------------------------------
278
308
  # Внутренние HTTP-методы
279
309
  # ------------------------------------------------------------------
280
310
 
281
- def _get(self, path: str, params: Optional[dict] = None) -> dict:
282
- headers = {**HEADERS_BASE, **self._auth.headers()}
283
- response = self._session.get(
311
+ def _ensure_session(self) -> aiohttp.ClientSession:
312
+ if self._session is None or self._session.closed:
313
+ raise RuntimeError(
314
+ "Сессия не открыта. Используйте `async with CloudTipsClient(auth) as client:` "
315
+ "или вызовите `await client.open()` перед первым запросом."
316
+ )
317
+ return self._session
318
+
319
+ async def _get(self, path: str, params: Optional[dict] = None) -> dict:
320
+ session = self._ensure_session()
321
+ headers = {**_HEADERS_BASE, **await self._auth.headers()}
322
+ async with session.get(
284
323
  self._base_url + path,
285
324
  headers=headers,
286
325
  params=params,
287
- timeout=15,
288
- )
289
- _raise_for_status(response)
290
- return response.json()
291
-
292
- def _post(self, path: str, json: Optional[dict] = None) -> dict:
293
- headers = {**HEADERS_BASE, **self._auth.headers()}
294
- response = self._session.post(
326
+ timeout=aiohttp.ClientTimeout(total=15),
327
+ ) as response:
328
+ await _raise_for_status(response)
329
+ return await response.json()
330
+
331
+ async def _post(self, path: str, json: Optional[dict] = None) -> dict:
332
+ session = self._ensure_session()
333
+ headers = {**_HEADERS_BASE, **await self._auth.headers()}
334
+ async with session.post(
295
335
  self._base_url + path,
296
336
  headers=headers,
297
337
  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(
338
+ timeout=aiohttp.ClientTimeout(total=15),
339
+ ) as response:
340
+ await _raise_for_status(response)
341
+ return await response.json()
342
+
343
+ async def _delete(self, path: str, json: Optional[dict] = None) -> dict:
344
+ session = self._ensure_session()
345
+ headers = {**_HEADERS_BASE, **await self._auth.headers()}
346
+ async with session.delete(
306
347
  self._base_url + path,
307
348
  headers=headers,
308
349
  json=json,
309
- timeout=15,
310
- )
311
- _raise_for_status(response)
312
- return response.json()
350
+ timeout=aiohttp.ClientTimeout(total=15),
351
+ ) as response:
352
+ await _raise_for_status(response)
353
+ return await response.json()
313
354
 
314
355
 
315
356
  class CloudTipsAPIError(Exception):
@@ -326,10 +367,10 @@ def _ensure_tz(dt: datetime) -> datetime:
326
367
  return dt if dt.tzinfo else dt.replace(tzinfo=_MSK)
327
368
 
328
369
 
329
- def _raise_for_status(response: requests.Response) -> None:
370
+ async def _raise_for_status(response: aiohttp.ClientResponse) -> None:
330
371
  if not response.ok:
331
372
  try:
332
- detail = response.json()
373
+ detail = await response.json()
333
374
  except Exception:
334
- detail = response.text
335
- raise CloudTipsAPIError(response.status_code, detail)
375
+ detail = await response.text()
376
+ raise CloudTipsAPIError(response.status, detail)
@@ -71,7 +71,7 @@ class Card:
71
71
 
72
72
  @dataclass
73
73
  class PayoutFeeInfo:
74
- text: str # текст с описанием комиссий
74
+ text: str
75
75
  downgrade_condition: str
76
76
  tinkoff_commission_hint: str
77
77
  instant_payout_commission_text: str
@@ -88,12 +88,12 @@ class PayoutFeeInfo:
88
88
 
89
89
  @dataclass
90
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 # текст подсказки
91
+ accumulated_amount: float
92
+ amount_to_deposit: float
93
+ commission: float
94
+ commission_percent: float
95
+ next_payout_date: Optional[str]
96
+ commission_hint: str
97
97
 
98
98
  @classmethod
99
99
  def from_dict(cls, data: dict) -> "AccumulationSummary":
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cloudtips"
7
- version = "0.3.1"
8
- description = "Неофициальная Python-библиотека для CloudTips (получение донатов, поллинг, обновление токенов)"
7
+ version = "0.4.0"
8
+ description = "Неофициальная асинхронная Python-библиотека для CloudTips (получение донатов, поллинг, обновление токенов)"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.9"
12
12
  dependencies = [
13
- "requests>=2.28",
13
+ "aiohttp>=3.9",
14
14
  ]
15
- keywords = ["cloudtips", "donations", "tips", "api", "payments"]
15
+ keywords = ["cloudtips", "donations", "tips", "api", "payments", "async", "asyncio"]
16
16
  classifiers = [
17
17
  "Programming Language :: Python :: 3",
18
18
  "Programming Language :: Python :: 3.9",
@@ -22,6 +22,7 @@ classifiers = [
22
22
  "License :: OSI Approved :: MIT License",
23
23
  "Operating System :: OS Independent",
24
24
  "Topic :: Internet :: WWW/HTTP",
25
+ "Framework :: AsyncIO",
25
26
  ]
26
27
 
27
28
  [project.urls]
cloudtips-0.3.1/PKG-INFO DELETED
@@ -1,151 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: cloudtips
3
- Version: 0.3.1
4
- Summary: Неофициальная Python-библиотека для CloudTips (получение донатов, поллинг, обновление токенов)
5
- Project-URL: Repository, https://github.com/IRRatium/cloudtips-api
6
- Project-URL: Issues, https://github.com/IRRatium/cloudtips-api/issues
7
- License: MIT
8
- License-File: LICENSE
9
- Keywords: api,cloudtips,donations,payments,tips
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Operating System :: OS Independent
12
- Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.9
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Topic :: Internet :: WWW/HTTP
18
- Requires-Python: >=3.9
19
- Requires-Dist: requests>=2.28
20
- Description-Content-Type: text/markdown
21
-
22
- # CloudtipsAPI
23
-
24
- Неофициальная Python-библиотека для [CloudTips](https://cloudtips.ru) — получение донатов, поллинг новых поступлений и автоматическое обновление токенов.
25
-
26
- ## Установка
27
-
28
- ```bash
29
- pip install cloudtips
30
- ```
31
-
32
- ## Быстрый старт
33
-
34
- ```python
35
- import json
36
- from cloudtips import CloudTipsAuth, CloudTipsClient, TokenData
37
-
38
- # Загружаем токены из файла (donate.json или своего хранилища)
39
- with open("donate.json") as f:
40
- config = json.load(f)
41
-
42
- # Колбэк вызывается каждый раз при обновлении токенов.
43
- # Refresh-токен одноразовый — обязательно сохраняйте новые!
44
- def on_token_refresh(token_data: TokenData):
45
- config["cloudtips_token"] = token_data.access_token
46
- config["cloudtips_refresh_token"] = token_data.refresh_token
47
- config["cloudtips_expires_at"] = token_data.expires_at
48
- with open("donate.json", "w") as f:
49
- json.dump(config, f, ensure_ascii=False, indent=2)
50
- print("Токены обновлены и сохранены.")
51
-
52
- auth = CloudTipsAuth(
53
- token=config["cloudtips_token"],
54
- refresh_token=config["cloudtips_refresh_token"],
55
- expires_at=config["cloudtips_expires_at"],
56
- on_token_refresh=on_token_refresh,
57
- )
58
-
59
- client = CloudTipsClient(auth)
60
- ```
61
-
62
- ## Получение донатов
63
-
64
- ```python
65
- # Все донаты
66
- donations = client.get_all_donations()
67
- for d in donations:
68
- print(d) # [2026-04-10 20:44] евгения → 50₽ — "оч крутой сервис"
69
-
70
- # Только за последние сутки
71
- from datetime import datetime, timedelta, timezone
72
-
73
- yesterday = datetime.now(tz=timezone.utc) - timedelta(days=1)
74
- recent = client.get_donations(since=yesterday)
75
- ```
76
-
77
- ## Поллинг новых донатов
78
-
79
- ### Вариант 1 — генератор (блокирует поток)
80
-
81
- ```python
82
- print("Слушаем новые донаты...")
83
- for donation in client.poll(interval=30):
84
- print(f"💰 {donation.name} задонатил {donation.amount}₽")
85
- if donation.comment:
86
- print(f" Комментарий: {donation.comment}")
87
- ```
88
-
89
- ### Вариант 2 — колбэк
90
-
91
- ```python
92
- def handle_donation(donation):
93
- print(f"Новый донат от {donation.name}: {donation.amount}₽")
94
-
95
- # Блокирующий вызов, удобно для простых скриптов
96
- client.poll(interval=15, callback=handle_donation)
97
- ```
98
-
99
- ### Вариант 3 — в отдельном потоке
100
-
101
- ```python
102
- import threading
103
-
104
- thread = threading.Thread(
105
- target=client.poll,
106
- kwargs={"interval": 30, "callback": handle_donation},
107
- daemon=True,
108
- )
109
- thread.start()
110
-
111
- # Основная логика программы продолжается...
112
- ```
113
-
114
- ## Структура `Donation`
115
-
116
- ```python
117
- @dataclass
118
- class Donation:
119
- transaction_id: int # уникальный ID транзакции
120
- name: str # имя донатера
121
- amount: int # сумма в рублях
122
- tg_id: int # Telegram ID
123
- comment: str # комментарий (может быть пустым)
124
- date: datetime # дата и время
125
-
126
- str(donation)
127
- # "[2026-04-10 23:04] Каспер → 200₽ — "спасибо за отличный сервис)""
128
- ```
129
-
130
- ## Обработка ошибок
131
-
132
- ```python
133
- from cloudtips import CloudTipsAuthError, CloudTipsAPIError
134
-
135
- try:
136
- donations = client.get_donations()
137
- except CloudTipsAuthError as e:
138
- print(f"Проблема с аутентификацией: {e}")
139
- except CloudTipsAPIError as e:
140
- print(f"Ошибка API (HTTP {e.status_code}): {e.detail}")
141
- ```
142
-
143
- ## Примечания
144
-
145
- - **Refresh-токен одноразовый.** После каждого обновления старый токен становится недействительным. Всегда передавайте `on_token_refresh` и сохраняйте новые токены.
146
- - Библиотека автоматически обновляет токен за 2 минуты до истечения.
147
- - Поллинг отслеживает уже виденные `transaction_id`, поэтому дублей не будет.
148
-
149
- ## Лицензия
150
-
151
- MIT
cloudtips-0.3.1/README.md DELETED
@@ -1,130 +0,0 @@
1
- # CloudtipsAPI
2
-
3
- Неофициальная Python-библиотека для [CloudTips](https://cloudtips.ru) — получение донатов, поллинг новых поступлений и автоматическое обновление токенов.
4
-
5
- ## Установка
6
-
7
- ```bash
8
- pip install cloudtips
9
- ```
10
-
11
- ## Быстрый старт
12
-
13
- ```python
14
- import json
15
- from cloudtips import CloudTipsAuth, CloudTipsClient, TokenData
16
-
17
- # Загружаем токены из файла (donate.json или своего хранилища)
18
- with open("donate.json") as f:
19
- config = json.load(f)
20
-
21
- # Колбэк вызывается каждый раз при обновлении токенов.
22
- # Refresh-токен одноразовый — обязательно сохраняйте новые!
23
- def on_token_refresh(token_data: TokenData):
24
- config["cloudtips_token"] = token_data.access_token
25
- config["cloudtips_refresh_token"] = token_data.refresh_token
26
- config["cloudtips_expires_at"] = token_data.expires_at
27
- with open("donate.json", "w") as f:
28
- json.dump(config, f, ensure_ascii=False, indent=2)
29
- print("Токены обновлены и сохранены.")
30
-
31
- auth = CloudTipsAuth(
32
- token=config["cloudtips_token"],
33
- refresh_token=config["cloudtips_refresh_token"],
34
- expires_at=config["cloudtips_expires_at"],
35
- on_token_refresh=on_token_refresh,
36
- )
37
-
38
- client = CloudTipsClient(auth)
39
- ```
40
-
41
- ## Получение донатов
42
-
43
- ```python
44
- # Все донаты
45
- donations = client.get_all_donations()
46
- for d in donations:
47
- print(d) # [2026-04-10 20:44] евгения → 50₽ — "оч крутой сервис"
48
-
49
- # Только за последние сутки
50
- from datetime import datetime, timedelta, timezone
51
-
52
- yesterday = datetime.now(tz=timezone.utc) - timedelta(days=1)
53
- recent = client.get_donations(since=yesterday)
54
- ```
55
-
56
- ## Поллинг новых донатов
57
-
58
- ### Вариант 1 — генератор (блокирует поток)
59
-
60
- ```python
61
- print("Слушаем новые донаты...")
62
- for donation in client.poll(interval=30):
63
- print(f"💰 {donation.name} задонатил {donation.amount}₽")
64
- if donation.comment:
65
- print(f" Комментарий: {donation.comment}")
66
- ```
67
-
68
- ### Вариант 2 — колбэк
69
-
70
- ```python
71
- def handle_donation(donation):
72
- print(f"Новый донат от {donation.name}: {donation.amount}₽")
73
-
74
- # Блокирующий вызов, удобно для простых скриптов
75
- client.poll(interval=15, callback=handle_donation)
76
- ```
77
-
78
- ### Вариант 3 — в отдельном потоке
79
-
80
- ```python
81
- import threading
82
-
83
- thread = threading.Thread(
84
- target=client.poll,
85
- kwargs={"interval": 30, "callback": handle_donation},
86
- daemon=True,
87
- )
88
- thread.start()
89
-
90
- # Основная логика программы продолжается...
91
- ```
92
-
93
- ## Структура `Donation`
94
-
95
- ```python
96
- @dataclass
97
- class Donation:
98
- transaction_id: int # уникальный ID транзакции
99
- name: str # имя донатера
100
- amount: int # сумма в рублях
101
- tg_id: int # Telegram ID
102
- comment: str # комментарий (может быть пустым)
103
- date: datetime # дата и время
104
-
105
- str(donation)
106
- # "[2026-04-10 23:04] Каспер → 200₽ — "спасибо за отличный сервис)""
107
- ```
108
-
109
- ## Обработка ошибок
110
-
111
- ```python
112
- from cloudtips import CloudTipsAuthError, CloudTipsAPIError
113
-
114
- try:
115
- donations = client.get_donations()
116
- except CloudTipsAuthError as e:
117
- print(f"Проблема с аутентификацией: {e}")
118
- except CloudTipsAPIError as e:
119
- print(f"Ошибка API (HTTP {e.status_code}): {e.detail}")
120
- ```
121
-
122
- ## Примечания
123
-
124
- - **Refresh-токен одноразовый.** После каждого обновления старый токен становится недействительным. Всегда передавайте `on_token_refresh` и сохраняйте новые токены.
125
- - Библиотека автоматически обновляет токен за 2 минуты до истечения.
126
- - Поллинг отслеживает уже виденные `transaction_id`, поэтому дублей не будет.
127
-
128
- ## Лицензия
129
-
130
- MIT
File without changes