cloudtips 0.4.0__tar.gz → 0.4.1__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.1/.github/workflows/docs.yml +31 -0
- cloudtips-0.4.1/.gitignore +4 -0
- cloudtips-0.4.1/CNAME +1 -0
- {cloudtips-0.4.0 → cloudtips-0.4.1}/PKG-INFO +47 -40
- {cloudtips-0.4.0 → cloudtips-0.4.1}/README.md +46 -39
- {cloudtips-0.4.0 → cloudtips-0.4.1}/cloudtips/__init__.py +1 -1
- {cloudtips-0.4.0 → cloudtips-0.4.1}/cloudtips/auth.py +10 -5
- {cloudtips-0.4.0 → cloudtips-0.4.1}/cloudtips/client.py +33 -35
- {cloudtips-0.4.0 → cloudtips-0.4.1}/cloudtips/models.py +8 -8
- cloudtips-0.4.1/docs/CNAME +1 -0
- cloudtips-0.4.1/docs/auth.md +94 -0
- cloudtips-0.4.1/docs/cards.md +73 -0
- cloudtips-0.4.1/docs/client.md +77 -0
- cloudtips-0.4.1/docs/donations.md +97 -0
- cloudtips-0.4.1/docs/errors.md +86 -0
- cloudtips-0.4.1/docs/index.md +74 -0
- cloudtips-0.4.1/docs/models/accumulation_summary.md +44 -0
- cloudtips-0.4.1/docs/models/card.md +65 -0
- cloudtips-0.4.1/docs/models/donation.md +60 -0
- cloudtips-0.4.1/docs/models/payout_fee_info.md +39 -0
- cloudtips-0.4.1/docs/models/receiver_profile.md +60 -0
- cloudtips-0.4.1/docs/models/token_data.md +38 -0
- cloudtips-0.4.1/docs/overrides/.gitkeep +0 -0
- cloudtips-0.4.1/docs/payouts.md +89 -0
- cloudtips-0.4.1/docs/polling.md +102 -0
- cloudtips-0.4.1/docs/profile.md +49 -0
- cloudtips-0.4.1/docs/quickstart.md +78 -0
- cloudtips-0.4.1/docs/stylesheets/irring.css +71 -0
- cloudtips-0.4.1/mkdocs.yml +54 -0
- {cloudtips-0.4.0 → cloudtips-0.4.1}/pyproject.toml +1 -1
- {cloudtips-0.4.0 → cloudtips-0.4.1}/.github/workflows/publish.yml +0 -0
- {cloudtips-0.4.0 → cloudtips-0.4.1}/LICENSE +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Deploy Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
paths:
|
|
8
|
+
- "docs/**"
|
|
9
|
+
- "mkdocs.yml"
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: write
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
deploy:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.11"
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: pip install mkdocs-material
|
|
29
|
+
|
|
30
|
+
- name: Deploy to GitHub Pages
|
|
31
|
+
run: mkdocs gh-deploy --force
|
cloudtips-0.4.1/CNAME
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
docs.cloudtips.irring.ru
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudtips
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Неофициальная асинхронная Python-библиотека для CloudTips (получение донатов, поллинг, обновление токенов)
|
|
5
5
|
Project-URL: Repository, https://github.com/IRRatium/cloudtips-api
|
|
6
6
|
Project-URL: Issues, https://github.com/IRRatium/cloudtips-api/issues
|
|
@@ -21,6 +21,17 @@ Requires-Dist: aiohttp>=3.9
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
|
|
23
23
|
# CloudtipsAPI
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://static.tildacdn.com/tild3431-6231-4938-b464-663831306266/Horiz.svg" alt="CloudTips" height="120">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<a href="https://pypi.org/project/cloudtips/"><img src="https://img.shields.io/pypi/v/cloudtips?color=blue&label=PyPI" alt="PyPI"></a>
|
|
30
|
+
<a href="https://pepy.tech/project/cloudtips"><img src="https://static.pepy.tech/badge/cloudtips" alt="Downloads"></a>
|
|
31
|
+
<a href="https://github.com/IRRatium/cloudtips-api/blob/main/LICENSE"><img src="https://img.shields.io/github/license/IRRatium/cloudtips-api" alt="License"></a>
|
|
32
|
+
<img src="https://img.shields.io/badge/python-3.9+-blue" alt="Python 3.9+">
|
|
33
|
+
<a href="https://docs.cloudtips.irring.ru"><img src="https://img.shields.io/badge/docs-docs.cloudtips.irring.ru-blue" alt="Docs"></a>
|
|
34
|
+
</p>
|
|
24
35
|
|
|
25
36
|
Неофициальная асинхронная Python-библиотека для [CloudTips](https://cloudtips.ru) — получение донатов, поллинг новых поступлений и автоматическое обновление токенов.
|
|
26
37
|
|
|
@@ -37,7 +48,6 @@ import asyncio
|
|
|
37
48
|
import json
|
|
38
49
|
from cloudtips import CloudTipsAuth, CloudTipsClient, TokenData
|
|
39
50
|
|
|
40
|
-
# Загружаем токены из файла
|
|
41
51
|
with open("donate.json") as f:
|
|
42
52
|
config = json.load(f)
|
|
43
53
|
|
|
@@ -50,7 +60,6 @@ async def on_token_refresh(token_data: TokenData):
|
|
|
50
60
|
config["cloudtips_expires_at"] = token_data.expires_at
|
|
51
61
|
with open("donate.json", "w") as f:
|
|
52
62
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
|
53
|
-
print("Токены обновлены и сохранены.")
|
|
54
63
|
|
|
55
64
|
auth = CloudTipsAuth(
|
|
56
65
|
token=config["cloudtips_token"],
|
|
@@ -64,8 +73,10 @@ async def main():
|
|
|
64
73
|
donations = await client.get_all_donations()
|
|
65
74
|
for d in donations:
|
|
66
75
|
print(d)
|
|
76
|
+
# [2024-01-15 14:30] Антон → 100₽ — "спасибо за контент"
|
|
67
77
|
|
|
68
78
|
asyncio.run(main())
|
|
79
|
+
|
|
69
80
|
```
|
|
70
81
|
|
|
71
82
|
## Получение донатов
|
|
@@ -75,13 +86,19 @@ async with CloudTipsClient(auth) as client:
|
|
|
75
86
|
# Все донаты за последние 24 часа
|
|
76
87
|
donations = await client.get_all_donations()
|
|
77
88
|
for d in donations:
|
|
78
|
-
print(d)
|
|
89
|
+
print(d.name) # Антон
|
|
90
|
+
print(d.amount) # 50.0
|
|
91
|
+
print(d.comment) # спасибо за сервис
|
|
92
|
+
print(d.date) # 2026-04-10 20:44:00+03:00
|
|
79
93
|
|
|
80
|
-
#
|
|
94
|
+
# За конкретный период
|
|
81
95
|
from datetime import datetime, timedelta, timezone
|
|
82
96
|
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
week_ago = datetime.now(tz=timezone.utc) - timedelta(days=7)
|
|
98
|
+
weekly = await client.get_all_donations(since=week_ago)
|
|
99
|
+
print(f"Всего за неделю: {len(weekly)} донатов")
|
|
100
|
+
print(f"Сумма: {sum(d.amount for d in weekly)}₽")
|
|
101
|
+
|
|
85
102
|
```
|
|
86
103
|
|
|
87
104
|
## Поллинг новых донатов
|
|
@@ -90,11 +107,11 @@ async with CloudTipsClient(auth) as client:
|
|
|
90
107
|
|
|
91
108
|
```python
|
|
92
109
|
async with CloudTipsClient(auth) as client:
|
|
93
|
-
print("Слушаем новые донаты...")
|
|
94
110
|
async for donation in client.poll(interval=30):
|
|
95
111
|
print(f"💰 {donation.name} задонатил {donation.amount}₽")
|
|
96
112
|
if donation.comment:
|
|
97
113
|
print(f" Комментарий: {donation.comment}")
|
|
114
|
+
|
|
98
115
|
```
|
|
99
116
|
|
|
100
117
|
### Вариант 2 — async-колбэк
|
|
@@ -105,10 +122,12 @@ async def handle_donation(donation):
|
|
|
105
122
|
# await bot.send_message(...)
|
|
106
123
|
|
|
107
124
|
async with CloudTipsClient(auth) as client:
|
|
108
|
-
|
|
125
|
+
async for _ in client.poll(interval=15, callback=handle_donation):
|
|
126
|
+
pass
|
|
127
|
+
|
|
109
128
|
```
|
|
110
129
|
|
|
111
|
-
### Вариант 3 —
|
|
130
|
+
### Вариант 3 — фоновая задача asyncio
|
|
112
131
|
|
|
113
132
|
```python
|
|
114
133
|
async def poll_task(client):
|
|
@@ -118,10 +137,11 @@ async def poll_task(client):
|
|
|
118
137
|
async def main():
|
|
119
138
|
async with CloudTipsClient(auth) as client:
|
|
120
139
|
task = asyncio.create_task(poll_task(client))
|
|
121
|
-
#
|
|
122
|
-
|
|
140
|
+
await asyncio.sleep(3600) # работаем час
|
|
141
|
+
task.cancel()
|
|
123
142
|
|
|
124
143
|
asyncio.run(main())
|
|
144
|
+
|
|
125
145
|
```
|
|
126
146
|
|
|
127
147
|
## Профиль, карты и баланс
|
|
@@ -130,13 +150,12 @@ asyncio.run(main())
|
|
|
130
150
|
async with CloudTipsClient(auth) as client:
|
|
131
151
|
# Профиль пользователя
|
|
132
152
|
me = await client.get_me()
|
|
133
|
-
print(me.full_name)
|
|
134
|
-
print(me.payout_method)
|
|
153
|
+
print(me.full_name) # Иван Иванов
|
|
154
|
+
print(me.payout_method) # Accumulation
|
|
135
155
|
|
|
136
156
|
# Привязанные карты
|
|
137
157
|
for card in await client.get_cards():
|
|
138
|
-
print(card)
|
|
139
|
-
print(card.token) # tk_89e6b3c6827afd4e9ccc36db2d22f
|
|
158
|
+
print(card) # MIR *0000 (T-BANK, до 00/00) [по умолчанию]
|
|
140
159
|
|
|
141
160
|
# Баланс к выводу
|
|
142
161
|
s = await client.get_accumulation_summary()
|
|
@@ -149,28 +168,14 @@ async with CloudTipsClient(auth) as client:
|
|
|
149
168
|
print(fee.text)
|
|
150
169
|
|
|
151
170
|
# Смена метода выплат
|
|
152
|
-
await client.set_payout_method("Instant")
|
|
153
|
-
await client.set_payout_method("Accumulation")
|
|
171
|
+
await client.set_payout_method("Instant") # мгновенно
|
|
172
|
+
await client.set_payout_method("Accumulation") # накопительно
|
|
154
173
|
|
|
155
174
|
# Удаление карты
|
|
156
175
|
for card in await client.get_cards():
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
## Структура `Donation`
|
|
176
|
+
if not card.is_default:
|
|
177
|
+
await client.delete_card(card.token)
|
|
161
178
|
|
|
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
179
|
```
|
|
175
180
|
|
|
176
181
|
## Обработка ошибок
|
|
@@ -180,20 +185,22 @@ from cloudtips import CloudTipsAuthError, CloudTipsAPIError
|
|
|
180
185
|
|
|
181
186
|
try:
|
|
182
187
|
async with CloudTipsClient(auth) as client:
|
|
183
|
-
donations = await client.
|
|
188
|
+
donations = await client.get_all_donations()
|
|
184
189
|
except CloudTipsAuthError as e:
|
|
185
190
|
print(f"Проблема с аутентификацией: {e}")
|
|
186
191
|
except CloudTipsAPIError as e:
|
|
187
192
|
print(f"Ошибка API (HTTP {e.status_code}): {e.detail}")
|
|
193
|
+
|
|
188
194
|
```
|
|
189
195
|
|
|
190
196
|
## Примечания
|
|
191
197
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
* **Refresh-токен одноразовый.** После каждого обновления старый токен становится недействительным. Всегда передавайте `on_token_refresh` и сохраняйте новые токены.
|
|
199
|
+
* `on_token_refresh` поддерживает как обычные (`def`), так и async-функции (`async def`).
|
|
200
|
+
* Библиотека автоматически обновляет токен за 2 минуты до истечения.
|
|
201
|
+
* Поллинг отслеживает уже виденные `transaction_id`, поэтому дублей не будет.
|
|
202
|
+
* Клиент — контекстный менеджер (`async with`), это рекомендуемый способ использования.
|
|
203
|
+
* Для логирования внутренних процессов и ошибок сети библиотека использует стандартный модуль `logging`. Настройте ваш логгер для отображения сообщений: `logging.basicConfig(level=logging.INFO)`.
|
|
197
204
|
|
|
198
205
|
## Лицензия
|
|
199
206
|
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
# CloudtipsAPI
|
|
2
|
+
<p align="center">
|
|
3
|
+
<img src="https://static.tildacdn.com/tild3431-6231-4938-b464-663831306266/Horiz.svg" alt="CloudTips" height="120">
|
|
4
|
+
</p>
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://pypi.org/project/cloudtips/"><img src="https://img.shields.io/pypi/v/cloudtips?color=blue&label=PyPI" alt="PyPI"></a>
|
|
8
|
+
<a href="https://pepy.tech/project/cloudtips"><img src="https://static.pepy.tech/badge/cloudtips" alt="Downloads"></a>
|
|
9
|
+
<a href="https://github.com/IRRatium/cloudtips-api/blob/main/LICENSE"><img src="https://img.shields.io/github/license/IRRatium/cloudtips-api" alt="License"></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/python-3.9+-blue" alt="Python 3.9+">
|
|
11
|
+
<a href="https://docs.cloudtips.irring.ru"><img src="https://img.shields.io/badge/docs-docs.cloudtips.irring.ru-blue" alt="Docs"></a>
|
|
12
|
+
</p>
|
|
2
13
|
|
|
3
14
|
Неофициальная асинхронная Python-библиотека для [CloudTips](https://cloudtips.ru) — получение донатов, поллинг новых поступлений и автоматическое обновление токенов.
|
|
4
15
|
|
|
@@ -15,7 +26,6 @@ import asyncio
|
|
|
15
26
|
import json
|
|
16
27
|
from cloudtips import CloudTipsAuth, CloudTipsClient, TokenData
|
|
17
28
|
|
|
18
|
-
# Загружаем токены из файла
|
|
19
29
|
with open("donate.json") as f:
|
|
20
30
|
config = json.load(f)
|
|
21
31
|
|
|
@@ -28,7 +38,6 @@ async def on_token_refresh(token_data: TokenData):
|
|
|
28
38
|
config["cloudtips_expires_at"] = token_data.expires_at
|
|
29
39
|
with open("donate.json", "w") as f:
|
|
30
40
|
json.dump(config, f, ensure_ascii=False, indent=2)
|
|
31
|
-
print("Токены обновлены и сохранены.")
|
|
32
41
|
|
|
33
42
|
auth = CloudTipsAuth(
|
|
34
43
|
token=config["cloudtips_token"],
|
|
@@ -42,8 +51,10 @@ async def main():
|
|
|
42
51
|
donations = await client.get_all_donations()
|
|
43
52
|
for d in donations:
|
|
44
53
|
print(d)
|
|
54
|
+
# [2024-01-15 14:30] Антон → 100₽ — "спасибо за контент"
|
|
45
55
|
|
|
46
56
|
asyncio.run(main())
|
|
57
|
+
|
|
47
58
|
```
|
|
48
59
|
|
|
49
60
|
## Получение донатов
|
|
@@ -53,13 +64,19 @@ async with CloudTipsClient(auth) as client:
|
|
|
53
64
|
# Все донаты за последние 24 часа
|
|
54
65
|
donations = await client.get_all_donations()
|
|
55
66
|
for d in donations:
|
|
56
|
-
print(d)
|
|
67
|
+
print(d.name) # Антон
|
|
68
|
+
print(d.amount) # 50.0
|
|
69
|
+
print(d.comment) # спасибо за сервис
|
|
70
|
+
print(d.date) # 2026-04-10 20:44:00+03:00
|
|
57
71
|
|
|
58
|
-
#
|
|
72
|
+
# За конкретный период
|
|
59
73
|
from datetime import datetime, timedelta, timezone
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
week_ago = datetime.now(tz=timezone.utc) - timedelta(days=7)
|
|
76
|
+
weekly = await client.get_all_donations(since=week_ago)
|
|
77
|
+
print(f"Всего за неделю: {len(weekly)} донатов")
|
|
78
|
+
print(f"Сумма: {sum(d.amount for d in weekly)}₽")
|
|
79
|
+
|
|
63
80
|
```
|
|
64
81
|
|
|
65
82
|
## Поллинг новых донатов
|
|
@@ -68,11 +85,11 @@ async with CloudTipsClient(auth) as client:
|
|
|
68
85
|
|
|
69
86
|
```python
|
|
70
87
|
async with CloudTipsClient(auth) as client:
|
|
71
|
-
print("Слушаем новые донаты...")
|
|
72
88
|
async for donation in client.poll(interval=30):
|
|
73
89
|
print(f"💰 {donation.name} задонатил {donation.amount}₽")
|
|
74
90
|
if donation.comment:
|
|
75
91
|
print(f" Комментарий: {donation.comment}")
|
|
92
|
+
|
|
76
93
|
```
|
|
77
94
|
|
|
78
95
|
### Вариант 2 — async-колбэк
|
|
@@ -83,10 +100,12 @@ async def handle_donation(donation):
|
|
|
83
100
|
# await bot.send_message(...)
|
|
84
101
|
|
|
85
102
|
async with CloudTipsClient(auth) as client:
|
|
86
|
-
|
|
103
|
+
async for _ in client.poll(interval=15, callback=handle_donation):
|
|
104
|
+
pass
|
|
105
|
+
|
|
87
106
|
```
|
|
88
107
|
|
|
89
|
-
### Вариант 3 —
|
|
108
|
+
### Вариант 3 — фоновая задача asyncio
|
|
90
109
|
|
|
91
110
|
```python
|
|
92
111
|
async def poll_task(client):
|
|
@@ -96,10 +115,11 @@ async def poll_task(client):
|
|
|
96
115
|
async def main():
|
|
97
116
|
async with CloudTipsClient(auth) as client:
|
|
98
117
|
task = asyncio.create_task(poll_task(client))
|
|
99
|
-
#
|
|
100
|
-
|
|
118
|
+
await asyncio.sleep(3600) # работаем час
|
|
119
|
+
task.cancel()
|
|
101
120
|
|
|
102
121
|
asyncio.run(main())
|
|
122
|
+
|
|
103
123
|
```
|
|
104
124
|
|
|
105
125
|
## Профиль, карты и баланс
|
|
@@ -108,13 +128,12 @@ asyncio.run(main())
|
|
|
108
128
|
async with CloudTipsClient(auth) as client:
|
|
109
129
|
# Профиль пользователя
|
|
110
130
|
me = await client.get_me()
|
|
111
|
-
print(me.full_name)
|
|
112
|
-
print(me.payout_method)
|
|
131
|
+
print(me.full_name) # Иван Иванов
|
|
132
|
+
print(me.payout_method) # Accumulation
|
|
113
133
|
|
|
114
134
|
# Привязанные карты
|
|
115
135
|
for card in await client.get_cards():
|
|
116
|
-
print(card)
|
|
117
|
-
print(card.token) # tk_89e6b3c6827afd4e9ccc36db2d22f
|
|
136
|
+
print(card) # MIR *0000 (T-BANK, до 00/00) [по умолчанию]
|
|
118
137
|
|
|
119
138
|
# Баланс к выводу
|
|
120
139
|
s = await client.get_accumulation_summary()
|
|
@@ -127,28 +146,14 @@ async with CloudTipsClient(auth) as client:
|
|
|
127
146
|
print(fee.text)
|
|
128
147
|
|
|
129
148
|
# Смена метода выплат
|
|
130
|
-
await client.set_payout_method("Instant")
|
|
131
|
-
await client.set_payout_method("Accumulation")
|
|
149
|
+
await client.set_payout_method("Instant") # мгновенно
|
|
150
|
+
await client.set_payout_method("Accumulation") # накопительно
|
|
132
151
|
|
|
133
152
|
# Удаление карты
|
|
134
153
|
for card in await client.get_cards():
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
## Структура `Donation`
|
|
154
|
+
if not card.is_default:
|
|
155
|
+
await client.delete_card(card.token)
|
|
139
156
|
|
|
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
157
|
```
|
|
153
158
|
|
|
154
159
|
## Обработка ошибок
|
|
@@ -158,20 +163,22 @@ from cloudtips import CloudTipsAuthError, CloudTipsAPIError
|
|
|
158
163
|
|
|
159
164
|
try:
|
|
160
165
|
async with CloudTipsClient(auth) as client:
|
|
161
|
-
donations = await client.
|
|
166
|
+
donations = await client.get_all_donations()
|
|
162
167
|
except CloudTipsAuthError as e:
|
|
163
168
|
print(f"Проблема с аутентификацией: {e}")
|
|
164
169
|
except CloudTipsAPIError as e:
|
|
165
170
|
print(f"Ошибка API (HTTP {e.status_code}): {e.detail}")
|
|
171
|
+
|
|
166
172
|
```
|
|
167
173
|
|
|
168
174
|
## Примечания
|
|
169
175
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
* **Refresh-токен одноразовый.** После каждого обновления старый токен становится недействительным. Всегда передавайте `on_token_refresh` и сохраняйте новые токены.
|
|
177
|
+
* `on_token_refresh` поддерживает как обычные (`def`), так и async-функции (`async def`).
|
|
178
|
+
* Библиотека автоматически обновляет токен за 2 минуты до истечения.
|
|
179
|
+
* Поллинг отслеживает уже виденные `transaction_id`, поэтому дублей не будет.
|
|
180
|
+
* Клиент — контекстный менеджер (`async with`), это рекомендуемый способ использования.
|
|
181
|
+
* Для логирования внутренних процессов и ошибок сети библиотека использует стандартный модуль `logging`. Настройте ваш логгер для отображения сообщений: `logging.basicConfig(level=logging.INFO)`.
|
|
175
182
|
|
|
176
183
|
## Лицензия
|
|
177
184
|
|
|
@@ -6,7 +6,7 @@ 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.4.
|
|
9
|
+
__version__ = "0.4.1"
|
|
10
10
|
__all__ = [
|
|
11
11
|
"CloudTipsAuth",
|
|
12
12
|
"CloudTipsAuthError",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Аутентификация CloudTips — управление access/refresh токенами.
|
|
3
3
|
"""
|
|
4
|
+
import asyncio
|
|
4
5
|
import time
|
|
5
6
|
from typing import Callable, Optional
|
|
6
7
|
|
|
@@ -30,8 +31,8 @@ class CloudTipsAuth:
|
|
|
30
31
|
await write_config(config)
|
|
31
32
|
|
|
32
33
|
auth = CloudTipsAuth(
|
|
33
|
-
token="...",
|
|
34
|
-
refresh_token="...",
|
|
34
|
+
token="eyJ...",
|
|
35
|
+
refresh_token="abc123...",
|
|
35
36
|
expires_at=1776099728.0,
|
|
36
37
|
on_token_refresh=save_tokens,
|
|
37
38
|
)
|
|
@@ -48,6 +49,7 @@ class CloudTipsAuth:
|
|
|
48
49
|
self._refresh_token = refresh_token
|
|
49
50
|
self._expires_at = expires_at
|
|
50
51
|
self._on_token_refresh = on_token_refresh
|
|
52
|
+
self._lock = asyncio.Lock() # Защита от Race Condition при параллельных запросах
|
|
51
53
|
|
|
52
54
|
# ------------------------------------------------------------------
|
|
53
55
|
# Public
|
|
@@ -56,7 +58,10 @@ class CloudTipsAuth:
|
|
|
56
58
|
async def get_token(self) -> str:
|
|
57
59
|
"""Возвращает актуальный access-токен, автоматически обновляя при необходимости."""
|
|
58
60
|
if self._is_expired():
|
|
59
|
-
|
|
61
|
+
async with self._lock:
|
|
62
|
+
# Повторная проверка: пока текущий таск ждал лока, другой таск уже мог обновить токен
|
|
63
|
+
if self._is_expired():
|
|
64
|
+
await self.refresh()
|
|
60
65
|
return self._token
|
|
61
66
|
|
|
62
67
|
@property
|
|
@@ -90,8 +95,7 @@ class CloudTipsAuth:
|
|
|
90
95
|
|
|
91
96
|
if self._on_token_refresh:
|
|
92
97
|
result = self._on_token_refresh(token_data)
|
|
93
|
-
|
|
94
|
-
if hasattr(result, "__await__"):
|
|
98
|
+
if asyncio.iscoroutine(result):
|
|
95
99
|
await result
|
|
96
100
|
|
|
97
101
|
return token_data
|
|
@@ -124,3 +128,4 @@ async def _raise_for_status(response: aiohttp.ClientResponse) -> None:
|
|
|
124
128
|
|
|
125
129
|
class CloudTipsAuthError(Exception):
|
|
126
130
|
"""Ошибка аутентификации CloudTips."""
|
|
131
|
+
pass
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Асинхронный клиент CloudTips API.
|
|
3
3
|
"""
|
|
4
4
|
import asyncio
|
|
5
|
+
import logging
|
|
5
6
|
from datetime import datetime, timezone, timedelta
|
|
6
7
|
from typing import AsyncIterator, Callable, List, Optional
|
|
7
8
|
|
|
@@ -13,6 +14,8 @@ from .models import AccumulationSummary, Card, Donation, PayoutFeeInfo, Receiver
|
|
|
13
14
|
_BASE_URL = "https://api.cloudtips.ru/api"
|
|
14
15
|
_MSK = timezone(timedelta(hours=3))
|
|
15
16
|
|
|
17
|
+
logger = logging.getLogger("cloudtips")
|
|
18
|
+
|
|
16
19
|
_HEADERS_BASE = {
|
|
17
20
|
"Accept": "application/json, text/plain, */*",
|
|
18
21
|
"Accept-Language": "ru-RU,ru;q=0.9",
|
|
@@ -40,26 +43,6 @@ class CloudTipsClient:
|
|
|
40
43
|
donations = await client.get_all_donations()
|
|
41
44
|
finally:
|
|
42
45
|
await client.close()
|
|
43
|
-
|
|
44
|
-
Пример быстрого старта::
|
|
45
|
-
|
|
46
|
-
import asyncio
|
|
47
|
-
from cloudtips import CloudTipsAuth, CloudTipsClient
|
|
48
|
-
|
|
49
|
-
auth = CloudTipsAuth(
|
|
50
|
-
token="...",
|
|
51
|
-
refresh_token="...",
|
|
52
|
-
expires_at=1776099728.0,
|
|
53
|
-
on_token_refresh=lambda td: print("Новый refresh:", td.refresh_token),
|
|
54
|
-
)
|
|
55
|
-
|
|
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())
|
|
63
46
|
"""
|
|
64
47
|
|
|
65
48
|
def __init__(self, auth: CloudTipsAuth, base_url: str = _BASE_URL) -> None:
|
|
@@ -125,11 +108,16 @@ class CloudTipsClient:
|
|
|
125
108
|
for item in raw_items:
|
|
126
109
|
if item.get("operationType") != "Transaction":
|
|
127
110
|
continue
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
amount_val = float(item.get("paymentAmount", 0))
|
|
114
|
+
except (ValueError, TypeError):
|
|
115
|
+
amount_val = 0.0
|
|
116
|
+
|
|
128
117
|
result.append(Donation.from_dict({
|
|
129
118
|
"transaction_id": item.get("transactionId", 0),
|
|
130
119
|
"name": (item.get("payerName") or "Аноним").strip(),
|
|
131
|
-
"amount":
|
|
132
|
-
"tg_id": 0,
|
|
120
|
+
"amount": amount_val,
|
|
133
121
|
"comment": (item.get("comment") or item.get("payerComment") or "").strip(),
|
|
134
122
|
"date": item.get("createdDate", now.isoformat()),
|
|
135
123
|
}))
|
|
@@ -173,43 +161,53 @@ class CloudTipsClient:
|
|
|
173
161
|
async for donation in client.poll(interval=15):
|
|
174
162
|
print(f"Новый донат: {donation}")
|
|
175
163
|
|
|
176
|
-
Или с async
|
|
164
|
+
Или с async-колбэком::
|
|
177
165
|
|
|
178
166
|
async def handle(donation):
|
|
179
167
|
await bot.send_message(chat_id, str(donation))
|
|
180
168
|
|
|
181
|
-
|
|
169
|
+
async for _ in client.poll(interval=15, callback=handle):
|
|
170
|
+
pass
|
|
182
171
|
|
|
183
172
|
:param interval: пауза между запросами в секундах
|
|
184
173
|
:param since: с какого момента начинать (по умолчанию — прямо сейчас)
|
|
185
|
-
:param callback: если передан —
|
|
174
|
+
:param callback: если передан — вызывается при каждом новом донате
|
|
186
175
|
"""
|
|
187
|
-
|
|
176
|
+
# Используем dict {id: date} вместо set для скользящего окна очистки памяти
|
|
177
|
+
last_seen_ids: dict[int, datetime] = {}
|
|
188
178
|
cursor = _ensure_tz(since or datetime.now(_MSK))
|
|
189
179
|
|
|
190
|
-
|
|
191
|
-
|
|
180
|
+
# Предзаполняем last_seen_ids только если since явно задан —
|
|
181
|
+
# иначе диапазон [now, now] вернёт пустой список и запрос бессмысленен
|
|
182
|
+
if since is not None:
|
|
183
|
+
for d in await self.get_all_donations(since=cursor):
|
|
184
|
+
last_seen_ids[d.transaction_id] = d.date
|
|
192
185
|
|
|
193
186
|
while True:
|
|
194
187
|
await asyncio.sleep(interval)
|
|
195
188
|
|
|
196
189
|
try:
|
|
197
|
-
|
|
190
|
+
# Небольшой оффсет назад (-10 сек) исключает пропуск донатов из-за задержек БД CloudTips
|
|
191
|
+
fresh = await self.get_all_donations(since=cursor - timedelta(seconds=10))
|
|
198
192
|
except Exception as exc:
|
|
199
|
-
|
|
193
|
+
logger.error("Ошибка при поллинге CloudTips: %s", exc, exc_info=True)
|
|
200
194
|
continue
|
|
201
195
|
|
|
202
196
|
for donation in fresh:
|
|
203
197
|
if donation.transaction_id not in last_seen_ids:
|
|
204
|
-
last_seen_ids.
|
|
198
|
+
last_seen_ids[donation.transaction_id] = donation.date
|
|
205
199
|
cursor = max(cursor, donation.date)
|
|
206
200
|
if callback:
|
|
207
201
|
result = callback(donation)
|
|
208
|
-
if
|
|
202
|
+
if asyncio.iscoroutine(result):
|
|
209
203
|
await result
|
|
210
204
|
else:
|
|
211
205
|
yield donation
|
|
212
206
|
|
|
207
|
+
# Очистка памяти: удаляем из кэша ID транзакций старше 6 часов от текущего курсора
|
|
208
|
+
limit_time = cursor - timedelta(hours=6)
|
|
209
|
+
last_seen_ids = {tid: dt for tid, dt in last_seen_ids.items() if dt >= limit_time}
|
|
210
|
+
|
|
213
211
|
# ------------------------------------------------------------------
|
|
214
212
|
# Профиль
|
|
215
213
|
# ------------------------------------------------------------------
|
|
@@ -223,7 +221,7 @@ class CloudTipsClient:
|
|
|
223
221
|
Пример::
|
|
224
222
|
|
|
225
223
|
me = await client.get_me()
|
|
226
|
-
print(me.full_name) #
|
|
224
|
+
print(me.full_name) # Иван Иванов
|
|
227
225
|
print(me.payout_method) # Accumulation
|
|
228
226
|
"""
|
|
229
227
|
data = await self._get("/receivers/me")
|
|
@@ -242,8 +240,8 @@ class CloudTipsClient:
|
|
|
242
240
|
Пример::
|
|
243
241
|
|
|
244
242
|
for card in await client.get_cards():
|
|
245
|
-
print(card) # MIR *
|
|
246
|
-
print(card.token) #
|
|
243
|
+
print(card) # MIR *0000 (BANK, до 01/28) [по умолчанию]
|
|
244
|
+
print(card.token) # tk_...
|
|
247
245
|
"""
|
|
248
246
|
data = await self._get("/cards")
|
|
249
247
|
return [Card.from_dict(item) for item in data.get("data", [])]
|