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 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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.