cloudtips 0.1.0__py3-none-any.whl
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/__init__.py +17 -0
- cloudtips/auth.py +122 -0
- cloudtips/client.py +173 -0
- cloudtips/models.py +48 -0
- cloudtips-0.1.0.dist-info/METADATA +151 -0
- cloudtips-0.1.0.dist-info/RECORD +8 -0
- cloudtips-0.1.0.dist-info/WHEEL +4 -0
- cloudtips-0.1.0.dist-info/licenses/LICENSE +21 -0
cloudtips/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloudtips — неофициальная Python-библиотека для CloudTips.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .auth import CloudTipsAuth, CloudTipsAuthError
|
|
6
|
+
from .client import CloudTipsAPIError, CloudTipsClient
|
|
7
|
+
from .models import Donation, TokenData
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CloudTipsAuth",
|
|
12
|
+
"CloudTipsAuthError",
|
|
13
|
+
"CloudTipsClient",
|
|
14
|
+
"CloudTipsAPIError",
|
|
15
|
+
"Donation",
|
|
16
|
+
"TokenData",
|
|
17
|
+
]
|
cloudtips/auth.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Callable, Optional
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from .models import TokenData
|
|
7
|
+
|
|
8
|
+
_TOKEN_URL = "https://identity.cloudtips.ru/connect/token"
|
|
9
|
+
_CLIENT_ID = "MobilePhone"
|
|
10
|
+
_EXPIRE_BUFFER = 120 # обновляем токен за 2 минуты до истечения
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CloudTipsAuth:
|
|
14
|
+
"""
|
|
15
|
+
Управляет access/refresh токенами CloudTips.
|
|
16
|
+
|
|
17
|
+
Параметр `on_token_refresh` — это колбэк, который вызывается каждый раз
|
|
18
|
+
после успешного обновления токенов. Используйте его, чтобы сохранить
|
|
19
|
+
новые токены в файл/БД/переменные окружения — refresh-токен одноразовый!
|
|
20
|
+
|
|
21
|
+
Пример::
|
|
22
|
+
|
|
23
|
+
def save_tokens(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
|
+
save_config(config)
|
|
28
|
+
|
|
29
|
+
auth = CloudTipsAuth(
|
|
30
|
+
token="...",
|
|
31
|
+
refresh_token="...",
|
|
32
|
+
expires_at=1776099728.0,
|
|
33
|
+
on_token_refresh=save_tokens,
|
|
34
|
+
)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
token: str,
|
|
40
|
+
refresh_token: str,
|
|
41
|
+
expires_at: float,
|
|
42
|
+
on_token_refresh: Optional[Callable[[TokenData], None]] = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
self._token = token
|
|
45
|
+
self._refresh_token = refresh_token
|
|
46
|
+
self._expires_at = expires_at
|
|
47
|
+
self._on_token_refresh = on_token_refresh
|
|
48
|
+
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
# Public
|
|
51
|
+
# ------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def token(self) -> str:
|
|
55
|
+
"""Возвращает актуальный access-токен, автоматически обновляя при необходимости."""
|
|
56
|
+
if self._is_expired():
|
|
57
|
+
self.refresh()
|
|
58
|
+
return self._token
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def expires_at(self) -> float:
|
|
62
|
+
return self._expires_at
|
|
63
|
+
|
|
64
|
+
def refresh(self) -> TokenData:
|
|
65
|
+
"""Принудительно обновляет токены через CloudTips Identity Server."""
|
|
66
|
+
response = requests.post(
|
|
67
|
+
_TOKEN_URL,
|
|
68
|
+
data={
|
|
69
|
+
"grant_type": "refresh_token",
|
|
70
|
+
"refresh_token": self._refresh_token,
|
|
71
|
+
"client_id": _CLIENT_ID,
|
|
72
|
+
},
|
|
73
|
+
timeout=10,
|
|
74
|
+
)
|
|
75
|
+
_raise_for_status(response)
|
|
76
|
+
|
|
77
|
+
data = response.json()
|
|
78
|
+
token_data = TokenData(
|
|
79
|
+
access_token=data["access_token"],
|
|
80
|
+
refresh_token=data["refresh_token"],
|
|
81
|
+
expires_at=time.time() + data["expires_in"],
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self._token = token_data.access_token
|
|
85
|
+
self._refresh_token = token_data.refresh_token
|
|
86
|
+
self._expires_at = token_data.expires_at
|
|
87
|
+
|
|
88
|
+
if self._on_token_refresh:
|
|
89
|
+
self._on_token_refresh(token_data)
|
|
90
|
+
|
|
91
|
+
return token_data
|
|
92
|
+
|
|
93
|
+
def headers(self) -> dict:
|
|
94
|
+
"""Готовые Authorization-заголовки для запросов."""
|
|
95
|
+
return {
|
|
96
|
+
"Authorization": f"Bearer {self.token}",
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
# Private
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def _is_expired(self) -> bool:
|
|
105
|
+
return time.time() >= self._expires_at - _EXPIRE_BUFFER
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _raise_for_status(response: requests.Response) -> None:
|
|
109
|
+
try:
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
except requests.HTTPError as e:
|
|
112
|
+
try:
|
|
113
|
+
detail = response.json()
|
|
114
|
+
except Exception:
|
|
115
|
+
detail = response.text
|
|
116
|
+
raise CloudTipsAuthError(
|
|
117
|
+
f"Ошибка при обновлении токена: {response.status_code} — {detail}"
|
|
118
|
+
) from e
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class CloudTipsAuthError(Exception):
|
|
122
|
+
"""Ошибка аутентификации CloudTips."""
|
cloudtips/client.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Клиент CloudTips API.
|
|
3
|
+
"""
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Callable, Iterator, List, Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .auth import CloudTipsAuth
|
|
11
|
+
from .models import Donation
|
|
12
|
+
|
|
13
|
+
_BASE_URL = "https://api.cloudtips.ru"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CloudTipsClient:
|
|
17
|
+
"""
|
|
18
|
+
Основной клиент для работы с CloudTips API.
|
|
19
|
+
|
|
20
|
+
Пример быстрого старта::
|
|
21
|
+
|
|
22
|
+
from cloudtips import CloudTipsClient, CloudTipsAuth
|
|
23
|
+
|
|
24
|
+
auth = CloudTipsAuth(
|
|
25
|
+
token="...",
|
|
26
|
+
refresh_token="...",
|
|
27
|
+
expires_at=1776099728.0,
|
|
28
|
+
on_token_refresh=lambda td: print("Новый токен:", td.access_token),
|
|
29
|
+
)
|
|
30
|
+
client = CloudTipsClient(auth)
|
|
31
|
+
|
|
32
|
+
# Получить все донаты
|
|
33
|
+
donations = client.get_donations()
|
|
34
|
+
|
|
35
|
+
# Поллинг новых донатов каждые 30 секунд
|
|
36
|
+
for donation in client.poll(interval=30):
|
|
37
|
+
print(donation)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, auth: CloudTipsAuth, base_url: str = _BASE_URL) -> None:
|
|
41
|
+
self._auth = auth
|
|
42
|
+
self._base_url = base_url.rstrip("/")
|
|
43
|
+
self._session = requests.Session()
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# Основные методы
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def get_donations(
|
|
50
|
+
self,
|
|
51
|
+
since: Optional[datetime] = None,
|
|
52
|
+
limit: int = 100,
|
|
53
|
+
page: int = 1,
|
|
54
|
+
) -> List[Donation]:
|
|
55
|
+
"""
|
|
56
|
+
Получить список донатов.
|
|
57
|
+
|
|
58
|
+
:param since: фильтр — только донаты после этого момента
|
|
59
|
+
:param limit: кол-во на страницу (max обычно 100)
|
|
60
|
+
:param page: номер страницы
|
|
61
|
+
:return: список :class:`Donation`
|
|
62
|
+
"""
|
|
63
|
+
params: dict = {"pageSize": limit, "page": page}
|
|
64
|
+
if since is not None:
|
|
65
|
+
params["from"] = since.isoformat()
|
|
66
|
+
|
|
67
|
+
data = self._get("/api/payments", params=params)
|
|
68
|
+
|
|
69
|
+
items = data if isinstance(data, list) else data.get("items", data.get("payments", []))
|
|
70
|
+
return [Donation.from_dict(item) for item in items]
|
|
71
|
+
|
|
72
|
+
def get_all_donations(self, since: Optional[datetime] = None) -> List[Donation]:
|
|
73
|
+
"""
|
|
74
|
+
Получить все донаты с автопагинацией.
|
|
75
|
+
|
|
76
|
+
:param since: фильтр по дате
|
|
77
|
+
:return: полный список :class:`Donation`
|
|
78
|
+
"""
|
|
79
|
+
result: List[Donation] = []
|
|
80
|
+
page = 1
|
|
81
|
+
while True:
|
|
82
|
+
batch = self.get_donations(since=since, limit=100, page=page)
|
|
83
|
+
if not batch:
|
|
84
|
+
break
|
|
85
|
+
result.extend(batch)
|
|
86
|
+
if len(batch) < 100:
|
|
87
|
+
break
|
|
88
|
+
page += 1
|
|
89
|
+
return sorted(result, key=lambda d: d.date)
|
|
90
|
+
|
|
91
|
+
def poll(
|
|
92
|
+
self,
|
|
93
|
+
interval: int = 30,
|
|
94
|
+
since: Optional[datetime] = None,
|
|
95
|
+
callback: Optional[Callable[[Donation], None]] = None,
|
|
96
|
+
) -> Iterator[Donation]:
|
|
97
|
+
"""
|
|
98
|
+
Генератор: бесконечный поллинг новых донатов.
|
|
99
|
+
|
|
100
|
+
Каждые ``interval`` секунд опрашивает API и отдаёт только **новые**
|
|
101
|
+
донаты (те, что появились после последнего запроса).
|
|
102
|
+
|
|
103
|
+
Использование как генератора::
|
|
104
|
+
|
|
105
|
+
for donation in client.poll(interval=15):
|
|
106
|
+
print(f"Новый донат: {donation}")
|
|
107
|
+
|
|
108
|
+
Или с колбэком (блокирующий режим)::
|
|
109
|
+
|
|
110
|
+
client.poll(interval=15, callback=handle_donation)
|
|
111
|
+
|
|
112
|
+
:param interval: пауза между запросами в секундах
|
|
113
|
+
:param since: с какого момента начинать (по умолчанию — «прямо сейчас»)
|
|
114
|
+
:param callback: если передан — метод блокируется и вызывает колбэк
|
|
115
|
+
"""
|
|
116
|
+
last_seen_ids: set = set()
|
|
117
|
+
cursor = since or datetime.now(tz=timezone.utc)
|
|
118
|
+
|
|
119
|
+
# Первый запрос — грузим уже известные, не отдаём как "новые"
|
|
120
|
+
for d in self.get_all_donations(since=cursor):
|
|
121
|
+
last_seen_ids.add(d.transaction_id)
|
|
122
|
+
|
|
123
|
+
while True:
|
|
124
|
+
time.sleep(interval)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
fresh = self.get_all_donations(since=cursor)
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
# Не падаем, просто пробуем снова на следующей итерации
|
|
130
|
+
print(f"[cloudtips] Ошибка при поллинге: {exc}")
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
for donation in fresh:
|
|
134
|
+
if donation.transaction_id not in last_seen_ids:
|
|
135
|
+
last_seen_ids.add(donation.transaction_id)
|
|
136
|
+
cursor = max(cursor, donation.date)
|
|
137
|
+
if callback:
|
|
138
|
+
callback(donation)
|
|
139
|
+
else:
|
|
140
|
+
yield donation
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# Вспомогательные
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def _get(self, path: str, params: Optional[dict] = None) -> dict:
|
|
147
|
+
url = self._base_url + path
|
|
148
|
+
response = self._session.get(
|
|
149
|
+
url,
|
|
150
|
+
headers=self._auth.headers(),
|
|
151
|
+
params=params,
|
|
152
|
+
timeout=15,
|
|
153
|
+
)
|
|
154
|
+
_raise_for_status(response)
|
|
155
|
+
return response.json()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class CloudTipsAPIError(Exception):
|
|
159
|
+
"""Ошибка запроса к CloudTips API."""
|
|
160
|
+
|
|
161
|
+
def __init__(self, status_code: int, detail: object) -> None:
|
|
162
|
+
self.status_code = status_code
|
|
163
|
+
self.detail = detail
|
|
164
|
+
super().__init__(f"HTTP {status_code}: {detail}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _raise_for_status(response: requests.Response) -> None:
|
|
168
|
+
if not response.ok:
|
|
169
|
+
try:
|
|
170
|
+
detail = response.json()
|
|
171
|
+
except Exception:
|
|
172
|
+
detail = response.text
|
|
173
|
+
raise CloudTipsAPIError(response.status_code, detail)
|
cloudtips/models.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Donation:
|
|
8
|
+
transaction_id: int
|
|
9
|
+
name: str
|
|
10
|
+
amount: int # рубли
|
|
11
|
+
tg_id: int
|
|
12
|
+
comment: str
|
|
13
|
+
date: datetime
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_dict(cls, data: dict) -> "Donation":
|
|
17
|
+
raw_date = data["date"]
|
|
18
|
+
# Убираем offset вида +03:00 → datetime aware через fromisoformat (3.11+)
|
|
19
|
+
# или ручной парсинг для 3.9/3.10
|
|
20
|
+
try:
|
|
21
|
+
dt = datetime.fromisoformat(raw_date)
|
|
22
|
+
except ValueError:
|
|
23
|
+
# fallback: обрезаем offset
|
|
24
|
+
dt = datetime.fromisoformat(raw_date[:19])
|
|
25
|
+
|
|
26
|
+
return cls(
|
|
27
|
+
transaction_id=data["transaction_id"],
|
|
28
|
+
name=data.get("name", ""),
|
|
29
|
+
amount=data["amount"],
|
|
30
|
+
tg_id=data.get("tg_id", 0),
|
|
31
|
+
comment=data.get("comment", ""),
|
|
32
|
+
date=dt,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
comment_part = f' — "{self.comment}"' if self.comment else ""
|
|
37
|
+
return (
|
|
38
|
+
f"[{self.date.strftime('%Y-%m-%d %H:%M')}] "
|
|
39
|
+
f"{self.name} → {self.amount}₽{comment_part}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class TokenData:
|
|
45
|
+
"""Новые токены, которые библиотека передаёт в on_token_refresh."""
|
|
46
|
+
access_token: str
|
|
47
|
+
refresh_token: str
|
|
48
|
+
expires_at: float # unix timestamp
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cloudtips
|
|
3
|
+
Version: 0.1.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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
cloudtips/__init__.py,sha256=SwfI5QmOmDg43EmugF4Z3JCxsBoglzt2zUPuiaRtUNY,408
|
|
2
|
+
cloudtips/auth.py,sha256=f5iCu21Ceve6uBhzJs6daE0GI5oWETtZmzMdhNnlaLk,4097
|
|
3
|
+
cloudtips/client.py,sha256=sttHkDJG1ePQ4AQ5PYwrhbzgscJ2GRNmiC7d_otzEUg,6208
|
|
4
|
+
cloudtips/models.py,sha256=oq_UWMrmKQKulAHcUzEqWJV6l_CGGJbnTsTg8-eviPI,1445
|
|
5
|
+
cloudtips-0.1.0.dist-info/METADATA,sha256=YzuxiI6I0N_tRe1WTRjmQHact7XuOi_M_Nkk-kNiQjU,5322
|
|
6
|
+
cloudtips-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
cloudtips-0.1.0.dist-info/licenses/LICENSE,sha256=t8Gzws0s2LPgRwASRw9MHAvwQJwVYTcFW0CoPTbllL0,1063
|
|
8
|
+
cloudtips-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IRRing
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|