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.
Files changed (32) hide show
  1. cloudtips-0.4.1/.github/workflows/docs.yml +31 -0
  2. cloudtips-0.4.1/.gitignore +4 -0
  3. cloudtips-0.4.1/CNAME +1 -0
  4. {cloudtips-0.4.0 → cloudtips-0.4.1}/PKG-INFO +47 -40
  5. {cloudtips-0.4.0 → cloudtips-0.4.1}/README.md +46 -39
  6. {cloudtips-0.4.0 → cloudtips-0.4.1}/cloudtips/__init__.py +1 -1
  7. {cloudtips-0.4.0 → cloudtips-0.4.1}/cloudtips/auth.py +10 -5
  8. {cloudtips-0.4.0 → cloudtips-0.4.1}/cloudtips/client.py +33 -35
  9. {cloudtips-0.4.0 → cloudtips-0.4.1}/cloudtips/models.py +8 -8
  10. cloudtips-0.4.1/docs/CNAME +1 -0
  11. cloudtips-0.4.1/docs/auth.md +94 -0
  12. cloudtips-0.4.1/docs/cards.md +73 -0
  13. cloudtips-0.4.1/docs/client.md +77 -0
  14. cloudtips-0.4.1/docs/donations.md +97 -0
  15. cloudtips-0.4.1/docs/errors.md +86 -0
  16. cloudtips-0.4.1/docs/index.md +74 -0
  17. cloudtips-0.4.1/docs/models/accumulation_summary.md +44 -0
  18. cloudtips-0.4.1/docs/models/card.md +65 -0
  19. cloudtips-0.4.1/docs/models/donation.md +60 -0
  20. cloudtips-0.4.1/docs/models/payout_fee_info.md +39 -0
  21. cloudtips-0.4.1/docs/models/receiver_profile.md +60 -0
  22. cloudtips-0.4.1/docs/models/token_data.md +38 -0
  23. cloudtips-0.4.1/docs/overrides/.gitkeep +0 -0
  24. cloudtips-0.4.1/docs/payouts.md +89 -0
  25. cloudtips-0.4.1/docs/polling.md +102 -0
  26. cloudtips-0.4.1/docs/profile.md +49 -0
  27. cloudtips-0.4.1/docs/quickstart.md +78 -0
  28. cloudtips-0.4.1/docs/stylesheets/irring.css +71 -0
  29. cloudtips-0.4.1/mkdocs.yml +54 -0
  30. {cloudtips-0.4.0 → cloudtips-0.4.1}/pyproject.toml +1 -1
  31. {cloudtips-0.4.0 → cloudtips-0.4.1}/.github/workflows/publish.yml +0 -0
  32. {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
@@ -0,0 +1,4 @@
1
+ site/
2
+ __pycache__/
3
+ *.pyc
4
+ .DS_Store
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.0
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) # [2026-04-10 20:44] евгения → 50₽ — "оч крутой сервис"
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
- yesterday = datetime.now(tz=timezone.utc) - timedelta(days=1)
84
- recent = await client.get_donations(since=yesterday)
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
- await client.poll(interval=15, callback=handle_donation)
125
+ async for _ in client.poll(interval=15, callback=handle_donation):
126
+ pass
127
+
109
128
  ```
110
129
 
111
- ### Вариант 3 — в фоновой задаче asyncio
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
- await task
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) # IRRing
134
- print(me.payout_method) # Accumulation
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) # MIR *3742 (T-BANK, до 08/34) [по умолчанию]
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
- await client.delete_card(card.token)
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.get_donations()
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
- - **Refresh-токен одноразовый.** После каждого обновления старый токен становится недействительным. Всегда передавайте `on_token_refresh` и сохраняйте новые токены.
193
- - `on_token_refresh` поддерживает как обычные (`def`), так и async-функции (`async def`).
194
- - Библиотека автоматически обновляет токен за 2 минуты до истечения.
195
- - Поллинг отслеживает уже виденные `transaction_id`, поэтому дублей не будет.
196
- - Клиент — контекстный менеджер (`async with`), это рекомендуемый способ использования.
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) # [2026-04-10 20:44] евгения → 50₽ — "оч крутой сервис"
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
- yesterday = datetime.now(tz=timezone.utc) - timedelta(days=1)
62
- recent = await client.get_donations(since=yesterday)
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
- await client.poll(interval=15, callback=handle_donation)
103
+ async for _ in client.poll(interval=15, callback=handle_donation):
104
+ pass
105
+
87
106
  ```
88
107
 
89
- ### Вариант 3 — в фоновой задаче asyncio
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
- await task
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) # IRRing
112
- print(me.payout_method) # Accumulation
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) # MIR *3742 (T-BANK, до 08/34) [по умолчанию]
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
- await client.delete_card(card.token)
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.get_donations()
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
- - **Refresh-токен одноразовый.** После каждого обновления старый токен становится недействительным. Всегда передавайте `on_token_refresh` и сохраняйте новые токены.
171
- - `on_token_refresh` поддерживает как обычные (`def`), так и async-функции (`async def`).
172
- - Библиотека автоматически обновляет токен за 2 минуты до истечения.
173
- - Поллинг отслеживает уже виденные `transaction_id`, поэтому дублей не будет.
174
- - Клиент — контекстный менеджер (`async with`), это рекомендуемый способ использования.
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.0"
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
- await self.refresh()
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
- # поддержка как async, так и обычных колбэков
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": int(item.get("paymentAmount", 0)),
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
- await client.poll(interval=15, callback=handle)
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
- last_seen_ids: set = set()
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
- for d in await self.get_all_donations(since=cursor):
191
- last_seen_ids.add(d.transaction_id)
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
- fresh = await self.get_all_donations(since=cursor)
190
+ # Небольшой оффсет назад (-10 сек) исключает пропуск донатов из-за задержек БД CloudTips
191
+ fresh = await self.get_all_donations(since=cursor - timedelta(seconds=10))
198
192
  except Exception as exc:
199
- print(f"[cloudtips] Ошибка при поллинге: {exc}")
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.add(donation.transaction_id)
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 hasattr(result, "__await__"):
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) # IRRing
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 *3742 (T-BANK, до 08/34) [по умолчанию]
246
- print(card.token) # tk_89e6b3c6827afd4e9ccc36db2d22f
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", [])]