cloudtips 0.3.1__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.1/PKG-INFO +207 -0
- cloudtips-0.4.1/README.md +185 -0
- {cloudtips-0.3.1 → cloudtips-0.4.1}/cloudtips/__init__.py +2 -2
- {cloudtips-0.3.1 → cloudtips-0.4.1}/cloudtips/auth.py +44 -35
- cloudtips-0.4.1/cloudtips/client.py +374 -0
- {cloudtips-0.3.1 → cloudtips-0.4.1}/cloudtips/models.py +15 -15
- 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.3.1 → cloudtips-0.4.1}/pyproject.toml +5 -4
- cloudtips-0.3.1/PKG-INFO +0 -151
- cloudtips-0.3.1/README.md +0 -130
- cloudtips-0.3.1/cloudtips/client.py +0 -335
- {cloudtips-0.3.1 → cloudtips-0.4.1}/.github/workflows/publish.yml +0 -0
- {cloudtips-0.3.1 → 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
|
cloudtips-0.4.1/PKG-INFO
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cloudtips
|
|
3
|
+
Version: 0.4.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,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
|
+
<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>
|
|
35
|
+
|
|
36
|
+
Неофициальная асинхронная Python-библиотека для [CloudTips](https://cloudtips.ru) — получение донатов, поллинг новых поступлений и автоматическое обновление токенов.
|
|
37
|
+
|
|
38
|
+
## Установка
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install cloudtips
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Быстрый старт
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
import json
|
|
49
|
+
from cloudtips import CloudTipsAuth, CloudTipsClient, TokenData
|
|
50
|
+
|
|
51
|
+
with open("donate.json") as f:
|
|
52
|
+
config = json.load(f)
|
|
53
|
+
|
|
54
|
+
# Колбэк вызывается каждый раз при обновлении токенов.
|
|
55
|
+
# Поддерживаются как async, так и обычные функции.
|
|
56
|
+
# Refresh-токен одноразовый — обязательно сохраняйте новые!
|
|
57
|
+
async def on_token_refresh(token_data: TokenData):
|
|
58
|
+
config["cloudtips_token"] = token_data.access_token
|
|
59
|
+
config["cloudtips_refresh_token"] = token_data.refresh_token
|
|
60
|
+
config["cloudtips_expires_at"] = token_data.expires_at
|
|
61
|
+
with open("donate.json", "w") as f:
|
|
62
|
+
json.dump(config, f, ensure_ascii=False, indent=2)
|
|
63
|
+
|
|
64
|
+
auth = CloudTipsAuth(
|
|
65
|
+
token=config["cloudtips_token"],
|
|
66
|
+
refresh_token=config["cloudtips_refresh_token"],
|
|
67
|
+
expires_at=config["cloudtips_expires_at"],
|
|
68
|
+
on_token_refresh=on_token_refresh,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def main():
|
|
72
|
+
async with CloudTipsClient(auth) as client:
|
|
73
|
+
donations = await client.get_all_donations()
|
|
74
|
+
for d in donations:
|
|
75
|
+
print(d)
|
|
76
|
+
# [2024-01-15 14:30] Антон → 100₽ — "спасибо за контент"
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Получение донатов
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
async with CloudTipsClient(auth) as client:
|
|
86
|
+
# Все донаты за последние 24 часа
|
|
87
|
+
donations = await client.get_all_donations()
|
|
88
|
+
for d in donations:
|
|
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
|
|
93
|
+
|
|
94
|
+
# За конкретный период
|
|
95
|
+
from datetime import datetime, timedelta, timezone
|
|
96
|
+
|
|
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
|
+
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Поллинг новых донатов
|
|
105
|
+
|
|
106
|
+
### Вариант 1 — async-генератор
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
async with CloudTipsClient(auth) as client:
|
|
110
|
+
async for donation in client.poll(interval=30):
|
|
111
|
+
print(f"💰 {donation.name} задонатил {donation.amount}₽")
|
|
112
|
+
if donation.comment:
|
|
113
|
+
print(f" Комментарий: {donation.comment}")
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Вариант 2 — async-колбэк
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
async def handle_donation(donation):
|
|
121
|
+
print(f"Новый донат от {donation.name}: {donation.amount}₽")
|
|
122
|
+
# await bot.send_message(...)
|
|
123
|
+
|
|
124
|
+
async with CloudTipsClient(auth) as client:
|
|
125
|
+
async for _ in client.poll(interval=15, callback=handle_donation):
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Вариант 3 — фоновая задача asyncio
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
async def poll_task(client):
|
|
134
|
+
async for donation in client.poll(interval=30):
|
|
135
|
+
print(f"Новый донат: {donation}")
|
|
136
|
+
|
|
137
|
+
async def main():
|
|
138
|
+
async with CloudTipsClient(auth) as client:
|
|
139
|
+
task = asyncio.create_task(poll_task(client))
|
|
140
|
+
await asyncio.sleep(3600) # работаем час
|
|
141
|
+
task.cancel()
|
|
142
|
+
|
|
143
|
+
asyncio.run(main())
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Профиль, карты и баланс
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
async with CloudTipsClient(auth) as client:
|
|
151
|
+
# Профиль пользователя
|
|
152
|
+
me = await client.get_me()
|
|
153
|
+
print(me.full_name) # Иван Иванов
|
|
154
|
+
print(me.payout_method) # Accumulation
|
|
155
|
+
|
|
156
|
+
# Привязанные карты
|
|
157
|
+
for card in await client.get_cards():
|
|
158
|
+
print(card) # MIR *0000 (T-BANK, до 00/00) [по умолчанию]
|
|
159
|
+
|
|
160
|
+
# Баланс к выводу
|
|
161
|
+
s = await client.get_accumulation_summary()
|
|
162
|
+
print(f"Накоплено: {s.accumulated_amount}₽")
|
|
163
|
+
print(f"Комиссия: {s.commission_percent}%")
|
|
164
|
+
print(f"Следующая выплата: {s.next_payout_date or 'не запланирована'}")
|
|
165
|
+
|
|
166
|
+
# Информация о комиссиях
|
|
167
|
+
fee = await client.get_payout_fee_info()
|
|
168
|
+
print(fee.text)
|
|
169
|
+
|
|
170
|
+
# Смена метода выплат
|
|
171
|
+
await client.set_payout_method("Instant") # мгновенно
|
|
172
|
+
await client.set_payout_method("Accumulation") # накопительно
|
|
173
|
+
|
|
174
|
+
# Удаление карты
|
|
175
|
+
for card in await client.get_cards():
|
|
176
|
+
if not card.is_default:
|
|
177
|
+
await client.delete_card(card.token)
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Обработка ошибок
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from cloudtips import CloudTipsAuthError, CloudTipsAPIError
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
async with CloudTipsClient(auth) as client:
|
|
188
|
+
donations = await client.get_all_donations()
|
|
189
|
+
except CloudTipsAuthError as e:
|
|
190
|
+
print(f"Проблема с аутентификацией: {e}")
|
|
191
|
+
except CloudTipsAPIError as e:
|
|
192
|
+
print(f"Ошибка API (HTTP {e.status_code}): {e.detail}")
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Примечания
|
|
197
|
+
|
|
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)`.
|
|
204
|
+
|
|
205
|
+
## Лицензия
|
|
206
|
+
|
|
207
|
+
MIT
|
|
@@ -0,0 +1,185 @@
|
|
|
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>
|
|
13
|
+
|
|
14
|
+
Неофициальная асинхронная Python-библиотека для [CloudTips](https://cloudtips.ru) — получение донатов, поллинг новых поступлений и автоматическое обновление токенов.
|
|
15
|
+
|
|
16
|
+
## Установка
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install cloudtips
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Быстрый старт
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import asyncio
|
|
26
|
+
import json
|
|
27
|
+
from cloudtips import CloudTipsAuth, CloudTipsClient, TokenData
|
|
28
|
+
|
|
29
|
+
with open("donate.json") as f:
|
|
30
|
+
config = json.load(f)
|
|
31
|
+
|
|
32
|
+
# Колбэк вызывается каждый раз при обновлении токенов.
|
|
33
|
+
# Поддерживаются как async, так и обычные функции.
|
|
34
|
+
# Refresh-токен одноразовый — обязательно сохраняйте новые!
|
|
35
|
+
async def on_token_refresh(token_data: TokenData):
|
|
36
|
+
config["cloudtips_token"] = token_data.access_token
|
|
37
|
+
config["cloudtips_refresh_token"] = token_data.refresh_token
|
|
38
|
+
config["cloudtips_expires_at"] = token_data.expires_at
|
|
39
|
+
with open("donate.json", "w") as f:
|
|
40
|
+
json.dump(config, f, ensure_ascii=False, indent=2)
|
|
41
|
+
|
|
42
|
+
auth = CloudTipsAuth(
|
|
43
|
+
token=config["cloudtips_token"],
|
|
44
|
+
refresh_token=config["cloudtips_refresh_token"],
|
|
45
|
+
expires_at=config["cloudtips_expires_at"],
|
|
46
|
+
on_token_refresh=on_token_refresh,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
async with CloudTipsClient(auth) as client:
|
|
51
|
+
donations = await client.get_all_donations()
|
|
52
|
+
for d in donations:
|
|
53
|
+
print(d)
|
|
54
|
+
# [2024-01-15 14:30] Антон → 100₽ — "спасибо за контент"
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Получение донатов
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
async with CloudTipsClient(auth) as client:
|
|
64
|
+
# Все донаты за последние 24 часа
|
|
65
|
+
donations = await client.get_all_donations()
|
|
66
|
+
for d in donations:
|
|
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
|
|
71
|
+
|
|
72
|
+
# За конкретный период
|
|
73
|
+
from datetime import datetime, timedelta, timezone
|
|
74
|
+
|
|
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
|
+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Поллинг новых донатов
|
|
83
|
+
|
|
84
|
+
### Вариант 1 — async-генератор
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
async with CloudTipsClient(auth) as client:
|
|
88
|
+
async for donation in client.poll(interval=30):
|
|
89
|
+
print(f"💰 {donation.name} задонатил {donation.amount}₽")
|
|
90
|
+
if donation.comment:
|
|
91
|
+
print(f" Комментарий: {donation.comment}")
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Вариант 2 — async-колбэк
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
async def handle_donation(donation):
|
|
99
|
+
print(f"Новый донат от {donation.name}: {donation.amount}₽")
|
|
100
|
+
# await bot.send_message(...)
|
|
101
|
+
|
|
102
|
+
async with CloudTipsClient(auth) as client:
|
|
103
|
+
async for _ in client.poll(interval=15, callback=handle_donation):
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Вариант 3 — фоновая задача asyncio
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
async def poll_task(client):
|
|
112
|
+
async for donation in client.poll(interval=30):
|
|
113
|
+
print(f"Новый донат: {donation}")
|
|
114
|
+
|
|
115
|
+
async def main():
|
|
116
|
+
async with CloudTipsClient(auth) as client:
|
|
117
|
+
task = asyncio.create_task(poll_task(client))
|
|
118
|
+
await asyncio.sleep(3600) # работаем час
|
|
119
|
+
task.cancel()
|
|
120
|
+
|
|
121
|
+
asyncio.run(main())
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Профиль, карты и баланс
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
async with CloudTipsClient(auth) as client:
|
|
129
|
+
# Профиль пользователя
|
|
130
|
+
me = await client.get_me()
|
|
131
|
+
print(me.full_name) # Иван Иванов
|
|
132
|
+
print(me.payout_method) # Accumulation
|
|
133
|
+
|
|
134
|
+
# Привязанные карты
|
|
135
|
+
for card in await client.get_cards():
|
|
136
|
+
print(card) # MIR *0000 (T-BANK, до 00/00) [по умолчанию]
|
|
137
|
+
|
|
138
|
+
# Баланс к выводу
|
|
139
|
+
s = await client.get_accumulation_summary()
|
|
140
|
+
print(f"Накоплено: {s.accumulated_amount}₽")
|
|
141
|
+
print(f"Комиссия: {s.commission_percent}%")
|
|
142
|
+
print(f"Следующая выплата: {s.next_payout_date or 'не запланирована'}")
|
|
143
|
+
|
|
144
|
+
# Информация о комиссиях
|
|
145
|
+
fee = await client.get_payout_fee_info()
|
|
146
|
+
print(fee.text)
|
|
147
|
+
|
|
148
|
+
# Смена метода выплат
|
|
149
|
+
await client.set_payout_method("Instant") # мгновенно
|
|
150
|
+
await client.set_payout_method("Accumulation") # накопительно
|
|
151
|
+
|
|
152
|
+
# Удаление карты
|
|
153
|
+
for card in await client.get_cards():
|
|
154
|
+
if not card.is_default:
|
|
155
|
+
await client.delete_card(card.token)
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Обработка ошибок
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from cloudtips import CloudTipsAuthError, CloudTipsAPIError
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
async with CloudTipsClient(auth) as client:
|
|
166
|
+
donations = await client.get_all_donations()
|
|
167
|
+
except CloudTipsAuthError as e:
|
|
168
|
+
print(f"Проблема с аутентификацией: {e}")
|
|
169
|
+
except CloudTipsAPIError as e:
|
|
170
|
+
print(f"Ошибка API (HTTP {e.status_code}): {e.detail}")
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Примечания
|
|
175
|
+
|
|
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)`.
|
|
182
|
+
|
|
183
|
+
## Лицензия
|
|
184
|
+
|
|
185
|
+
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.1"
|
|
10
10
|
__all__ = [
|
|
11
11
|
"CloudTipsAuth",
|
|
12
12
|
"CloudTipsAuthError",
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Аутентификация CloudTips — управление access/refresh токенами.
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
1
5
|
import time
|
|
2
6
|
from typing import Callable, Optional
|
|
3
7
|
|
|
4
|
-
import
|
|
8
|
+
import aiohttp
|
|
5
9
|
|
|
6
10
|
from .models import TokenData
|
|
7
11
|
|
|
@@ -14,21 +18,21 @@ class CloudTipsAuth:
|
|
|
14
18
|
"""
|
|
15
19
|
Управляет access/refresh токенами CloudTips.
|
|
16
20
|
|
|
17
|
-
Параметр
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
Параметр ``on_token_refresh`` — колбэк, вызываемый после каждого
|
|
22
|
+
успешного обновления токенов. Refresh-токен одноразовый, поэтому
|
|
23
|
+
обязательно сохраняйте новые значения.
|
|
20
24
|
|
|
21
25
|
Пример::
|
|
22
26
|
|
|
23
|
-
def save_tokens(token_data: TokenData):
|
|
27
|
+
async def save_tokens(token_data: TokenData):
|
|
24
28
|
config["cloudtips_token"] = token_data.access_token
|
|
25
29
|
config["cloudtips_refresh_token"] = token_data.refresh_token
|
|
26
30
|
config["cloudtips_expires_at"] = token_data.expires_at
|
|
27
|
-
|
|
31
|
+
await write_config(config)
|
|
28
32
|
|
|
29
33
|
auth = CloudTipsAuth(
|
|
30
|
-
token="...",
|
|
31
|
-
refresh_token="...",
|
|
34
|
+
token="eyJ...",
|
|
35
|
+
refresh_token="abc123...",
|
|
32
36
|
expires_at=1776099728.0,
|
|
33
37
|
on_token_refresh=save_tokens,
|
|
34
38
|
)
|
|
@@ -45,36 +49,40 @@ class CloudTipsAuth:
|
|
|
45
49
|
self._refresh_token = refresh_token
|
|
46
50
|
self._expires_at = expires_at
|
|
47
51
|
self._on_token_refresh = on_token_refresh
|
|
52
|
+
self._lock = asyncio.Lock() # Защита от Race Condition при параллельных запросах
|
|
48
53
|
|
|
49
54
|
# ------------------------------------------------------------------
|
|
50
55
|
# Public
|
|
51
56
|
# ------------------------------------------------------------------
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
def token(self) -> str:
|
|
58
|
+
async def get_token(self) -> str:
|
|
55
59
|
"""Возвращает актуальный access-токен, автоматически обновляя при необходимости."""
|
|
56
60
|
if self._is_expired():
|
|
57
|
-
self.
|
|
61
|
+
async with self._lock:
|
|
62
|
+
# Повторная проверка: пока текущий таск ждал лока, другой таск уже мог обновить токен
|
|
63
|
+
if self._is_expired():
|
|
64
|
+
await self.refresh()
|
|
58
65
|
return self._token
|
|
59
66
|
|
|
60
67
|
@property
|
|
61
68
|
def expires_at(self) -> float:
|
|
62
69
|
return self._expires_at
|
|
63
70
|
|
|
64
|
-
def refresh(self) -> TokenData:
|
|
71
|
+
async def refresh(self) -> TokenData:
|
|
65
72
|
"""Принудительно обновляет токены через CloudTips Identity Server."""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
async with aiohttp.ClientSession() as session:
|
|
74
|
+
async with session.post(
|
|
75
|
+
_TOKEN_URL,
|
|
76
|
+
data={
|
|
77
|
+
"grant_type": "refresh_token",
|
|
78
|
+
"refresh_token": self._refresh_token,
|
|
79
|
+
"client_id": _CLIENT_ID,
|
|
80
|
+
},
|
|
81
|
+
timeout=aiohttp.ClientTimeout(total=10),
|
|
82
|
+
) as response:
|
|
83
|
+
await _raise_for_status(response)
|
|
84
|
+
data = await response.json()
|
|
76
85
|
|
|
77
|
-
data = response.json()
|
|
78
86
|
token_data = TokenData(
|
|
79
87
|
access_token=data["access_token"],
|
|
80
88
|
refresh_token=data["refresh_token"],
|
|
@@ -86,15 +94,17 @@ class CloudTipsAuth:
|
|
|
86
94
|
self._expires_at = token_data.expires_at
|
|
87
95
|
|
|
88
96
|
if self._on_token_refresh:
|
|
89
|
-
self._on_token_refresh(token_data)
|
|
97
|
+
result = self._on_token_refresh(token_data)
|
|
98
|
+
if asyncio.iscoroutine(result):
|
|
99
|
+
await result
|
|
90
100
|
|
|
91
101
|
return token_data
|
|
92
102
|
|
|
93
|
-
def headers(self) -> dict:
|
|
103
|
+
async def headers(self) -> dict:
|
|
94
104
|
"""Готовые Authorization-заголовки для запросов."""
|
|
95
105
|
return {
|
|
96
|
-
"Authorization": f"Bearer {self.
|
|
97
|
-
"Content-Type":
|
|
106
|
+
"Authorization": f"Bearer {await self.get_token()}",
|
|
107
|
+
"Content-Type": "application/json",
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
# ------------------------------------------------------------------
|
|
@@ -105,18 +115,17 @@ class CloudTipsAuth:
|
|
|
105
115
|
return time.time() >= self._expires_at - _EXPIRE_BUFFER
|
|
106
116
|
|
|
107
117
|
|
|
108
|
-
def _raise_for_status(response:
|
|
109
|
-
|
|
110
|
-
response.raise_for_status()
|
|
111
|
-
except requests.HTTPError as e:
|
|
118
|
+
async def _raise_for_status(response: aiohttp.ClientResponse) -> None:
|
|
119
|
+
if not response.ok:
|
|
112
120
|
try:
|
|
113
|
-
detail = response.json()
|
|
121
|
+
detail = await response.json()
|
|
114
122
|
except Exception:
|
|
115
|
-
detail = response.text
|
|
123
|
+
detail = await response.text()
|
|
116
124
|
raise CloudTipsAuthError(
|
|
117
|
-
f"Ошибка при обновлении токена: {response.
|
|
118
|
-
)
|
|
125
|
+
f"Ошибка при обновлении токена: {response.status} — {detail}"
|
|
126
|
+
)
|
|
119
127
|
|
|
120
128
|
|
|
121
129
|
class CloudTipsAuthError(Exception):
|
|
122
130
|
"""Ошибка аутентификации CloudTips."""
|
|
131
|
+
pass
|