cloudtips 0.3.0__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.
- cloudtips-0.4.0/PKG-INFO +200 -0
- cloudtips-0.4.0/README.md +178 -0
- {cloudtips-0.3.0 → cloudtips-0.4.0}/cloudtips/__init__.py +2 -2
- {cloudtips-0.3.0 → cloudtips-0.4.0}/cloudtips/auth.py +37 -33
- {cloudtips-0.3.0 → cloudtips-0.4.0}/cloudtips/client.py +134 -93
- {cloudtips-0.3.0 → cloudtips-0.4.0}/cloudtips/models.py +9 -7
- {cloudtips-0.3.0 → cloudtips-0.4.0}/pyproject.toml +5 -4
- cloudtips-0.3.0/PKG-INFO +0 -151
- cloudtips-0.3.0/README.md +0 -130
- {cloudtips-0.3.0 → cloudtips-0.4.0}/.github/workflows/publish.yml +0 -0
- {cloudtips-0.3.0 → cloudtips-0.4.0}/LICENSE +0 -0
cloudtips-0.4.0/PKG-INFO
ADDED
|
@@ -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.
|
|
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
|
|
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
|
-
Параметр
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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.
|
|
97
|
-
"Content-Type":
|
|
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:
|
|
109
|
-
|
|
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.
|
|
118
|
-
)
|
|
121
|
+
f"Ошибка при обновлении токена: {response.status} — {detail}"
|
|
122
|
+
)
|
|
119
123
|
|
|
120
124
|
|
|
121
125
|
class CloudTipsAuthError(Exception):
|
|
@@ -1,34 +1,50 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Асинхронный клиент CloudTips API.
|
|
3
3
|
"""
|
|
4
|
-
import
|
|
4
|
+
import asyncio
|
|
5
5
|
from datetime import datetime, timezone, timedelta
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import AsyncIterator, Callable, List, Optional
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import aiohttp
|
|
9
9
|
|
|
10
10
|
from .auth import CloudTipsAuth
|
|
11
|
-
from .models import
|
|
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
|
-
|
|
17
|
-
"Accept":
|
|
16
|
+
_HEADERS_BASE = {
|
|
17
|
+
"Accept": "application/json, text/plain, */*",
|
|
18
18
|
"Accept-Language": "ru-RU,ru;q=0.9",
|
|
19
|
-
"Origin":
|
|
20
|
-
"Referer":
|
|
21
|
-
"User-Agent":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 =
|
|
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
|
-
) ->
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def _post(self, path: str, json: Optional[dict] = None) -> dict:
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def _delete(self, path: str, json: Optional[dict] = None) -> dict:
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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:
|
|
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.
|
|
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]
|
|
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":
|
|
@@ -155,6 +155,8 @@ class ReceiverProfile:
|
|
|
155
155
|
)
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
@dataclass
|
|
159
|
+
class TokenData:
|
|
158
160
|
"""Новые токены, которые библиотека передаёт в on_token_refresh."""
|
|
159
161
|
access_token: str
|
|
160
162
|
refresh_token: str
|
|
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cloudtips"
|
|
7
|
-
version = "0.
|
|
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
|
-
"
|
|
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.0/PKG-INFO
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: cloudtips
|
|
3
|
-
Version: 0.3.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,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.0/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
|
|
File without changes
|