cloudtips 0.1.1__tar.gz → 0.3.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudtips
3
- Version: 0.1.1
3
+ Version: 0.3.0
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
@@ -4,14 +4,18 @@ cloudtips — неофициальная Python-библиотека для Clou
4
4
 
5
5
  from .auth import CloudTipsAuth, CloudTipsAuthError
6
6
  from .client import CloudTipsAPIError, CloudTipsClient
7
- from .models import Donation, TokenData
7
+ from .models import AccumulationSummary, Card, Donation, PayoutFeeInfo, ReceiverProfile, TokenData
8
8
 
9
- __version__ = "0.1.0"
9
+ __version__ = "0.3.0"
10
10
  __all__ = [
11
11
  "CloudTipsAuth",
12
12
  "CloudTipsAuthError",
13
13
  "CloudTipsClient",
14
14
  "CloudTipsAPIError",
15
15
  "Donation",
16
+ "Card",
17
+ "PayoutFeeInfo",
18
+ "AccumulationSummary",
19
+ "ReceiverProfile",
16
20
  "TokenData",
17
21
  ]
@@ -8,7 +8,7 @@ from typing import Callable, Iterator, List, Optional
8
8
  import requests
9
9
 
10
10
  from .auth import CloudTipsAuth
11
- from .models import Donation
11
+ from .models import Donation, Card, PayoutFeeInfo, AccumulationSummary, ReceiverProfile
12
12
 
13
13
  _BASE_URL = "https://api.cloudtips.ru/api"
14
14
  _MSK = timezone(timedelta(hours=3))
@@ -38,12 +38,9 @@ class CloudTipsClient:
38
38
  )
39
39
  client = CloudTipsClient(auth)
40
40
 
41
- # Получить донаты за последние 30 дней
42
- donations = client.get_all_donations(since=datetime.now() - timedelta(days=30))
43
-
44
- # Поллинг новых донатов каждые 30 секунд
45
- for donation in client.poll(interval=30):
46
- print(donation)
41
+ donations = client.get_all_donations()
42
+ cards = client.get_cards()
43
+ summary = client.get_accumulation_summary()
47
44
  """
48
45
 
49
46
  def __init__(self, auth: CloudTipsAuth, base_url: str = _BASE_URL) -> None:
@@ -52,7 +49,7 @@ class CloudTipsClient:
52
49
  self._session = requests.Session()
53
50
 
54
51
  # ------------------------------------------------------------------
55
- # Основные методы
52
+ # Донаты
56
53
  # ------------------------------------------------------------------
57
54
 
58
55
  def get_donations(
@@ -130,9 +127,6 @@ class CloudTipsClient:
130
127
  """
131
128
  Генератор: бесконечный поллинг новых донатов.
132
129
 
133
- Каждые ``interval`` секунд опрашивает API и отдаёт только **новые**
134
- донаты (те, что появились после последнего запроса).
135
-
136
130
  Использование как генератора::
137
131
 
138
132
  for donation in client.poll(interval=15):
@@ -149,7 +143,6 @@ class CloudTipsClient:
149
143
  last_seen_ids: set = set()
150
144
  cursor = _ensure_tz(since or datetime.now(_MSK))
151
145
 
152
- # Первый запрос — запоминаем уже существующие, не отдаём как «новые»
153
146
  for d in self.get_all_donations(since=cursor):
154
147
  last_seen_ids.add(d.transaction_id)
155
148
 
@@ -159,7 +152,6 @@ class CloudTipsClient:
159
152
  try:
160
153
  fresh = self.get_all_donations(since=cursor)
161
154
  except Exception as exc:
162
- # Не падаем при временных ошибках, пробуем снова
163
155
  print(f"[cloudtips] Ошибка при поллинге: {exc}")
164
156
  continue
165
157
 
@@ -173,7 +165,117 @@ class CloudTipsClient:
173
165
  yield donation
174
166
 
175
167
  # ------------------------------------------------------------------
176
- # Вспомогательные
168
+ # Профиль
169
+ # ------------------------------------------------------------------
170
+
171
+ def get_me(self) -> ReceiverProfile:
172
+ """
173
+ Получить профиль текущего пользователя.
174
+
175
+ Содержит имя, телефон, метод выплат, лимиты сумм и другие данные.
176
+
177
+ :return: :class:`ReceiverProfile`
178
+
179
+ Пример::
180
+
181
+ me = client.get_me()
182
+ print(me.full_name) # IRRing
183
+ print(me.payout_method) # Accumulation
184
+ print(me.available_amount_min, me.available_amount_max) # 49.0 3000.0
185
+ """
186
+ data = self._get("/receivers/me")
187
+ return ReceiverProfile.from_dict(data.get("data", {}))
188
+
189
+ # ------------------------------------------------------------------
190
+ # Карты
191
+ # ------------------------------------------------------------------
192
+
193
+ def get_cards(self) -> List[Card]:
194
+ """
195
+ Получить список привязанных карт.
196
+
197
+ :return: список :class:`Card`
198
+
199
+ Пример::
200
+
201
+ for card in client.get_cards():
202
+ print(card) # MIR *3742 (T-BANK (TINKOFF), до 08/34) [по умолчанию]
203
+ print(card.token) # tk_89e6b3c6827afd4e9ccc36db2d22f
204
+ """
205
+ data = self._get("/cards")
206
+ return [Card.from_dict(item) for item in data.get("data", [])]
207
+
208
+ def delete_card(self, card_token: str) -> bool:
209
+ """
210
+ Удалить привязанную карту.
211
+
212
+ :param card_token: токен карты (``card.token``)
213
+ :return: ``True`` если удаление прошло успешно
214
+
215
+ Пример::
216
+
217
+ for card in client.get_cards():
218
+ client.delete_card(card.token)
219
+ """
220
+ data = self._delete("/cards", json={"cardToken": card_token})
221
+ return data.get("succeed", False)
222
+
223
+ # ------------------------------------------------------------------
224
+ # Выплаты и баланс
225
+ # ------------------------------------------------------------------
226
+
227
+ def get_payout_fee_info(self) -> PayoutFeeInfo:
228
+ """
229
+ Получить информацию о комиссиях при выводе средств.
230
+
231
+ :return: :class:`PayoutFeeInfo`
232
+
233
+ Пример::
234
+
235
+ fee = client.get_payout_fee_info()
236
+ print(fee.text)
237
+ # Стоимость вывода денег на карты Т-Банка — 5%
238
+ # Стоимость вывода денег на карты других банков — 7%*
239
+ """
240
+ data = self._get("/payout/fee/info")
241
+ return PayoutFeeInfo.from_dict(data.get("data", {}))
242
+
243
+ def get_accumulation_summary(self) -> AccumulationSummary:
244
+ """
245
+ Получить сводку по накопленным средствам (баланс к выводу).
246
+
247
+ :return: :class:`AccumulationSummary`
248
+
249
+ Пример::
250
+
251
+ s = client.get_accumulation_summary()
252
+ print(f"Накоплено: {s.accumulated_amount}₽")
253
+ print(f"Комиссия: {s.commission_percent}%")
254
+ print(f"Следующая выплата: {s.next_payout_date or 'не запланирована'}")
255
+ """
256
+ data = self._get("/accumulations/summary")
257
+ return AccumulationSummary.from_dict(data.get("data", {}))
258
+
259
+ def get_payout_method(self) -> str:
260
+ """
261
+ Получить текущий метод выплат.
262
+
263
+ :return: ``"Instant"`` или ``"Accumulation"``
264
+ """
265
+ return self.get_me().payout_method
266
+
267
+ def set_payout_method(self, method: str = "Instant") -> bool:
268
+ """
269
+ Установить метод выплат.
270
+
271
+ :param method: ``"Instant"`` (мгновенно) или ``"Accumulation"`` (накопительно)
272
+ :return: ``True`` если успешно
273
+ """
274
+ data = self._post("/receivers/payout-method", json={"payoutMethod": method})
275
+ return data.get("succeed", False)
276
+
277
+ # ------------------------------------------------------------------
278
+ # Внутренние HTTP-методы
177
279
  # ------------------------------------------------------------------
178
280
 
179
281
  def _get(self, path: str, params: Optional[dict] = None) -> dict:
@@ -187,6 +289,28 @@ class CloudTipsClient:
187
289
  _raise_for_status(response)
188
290
  return response.json()
189
291
 
292
+ def _post(self, path: str, json: Optional[dict] = None) -> dict:
293
+ headers = {**HEADERS_BASE, **self._auth.headers()}
294
+ response = self._session.post(
295
+ self._base_url + path,
296
+ headers=headers,
297
+ json=json,
298
+ timeout=15,
299
+ )
300
+ _raise_for_status(response)
301
+ return response.json()
302
+
303
+ def _delete(self, path: str, json: Optional[dict] = None) -> dict:
304
+ headers = {**HEADERS_BASE, **self._auth.headers()}
305
+ response = self._session.delete(
306
+ self._base_url + path,
307
+ headers=headers,
308
+ json=json,
309
+ timeout=15,
310
+ )
311
+ _raise_for_status(response)
312
+ return response.json()
313
+
190
314
 
191
315
  class CloudTipsAPIError(Exception):
192
316
  """Ошибка запроса к CloudTips API."""
@@ -0,0 +1,161 @@
1
+ from dataclasses import dataclass
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
+ try:
19
+ dt = datetime.fromisoformat(raw_date)
20
+ except ValueError:
21
+ dt = datetime.fromisoformat(raw_date[:19])
22
+
23
+ return cls(
24
+ transaction_id=data["transaction_id"],
25
+ name=data.get("name", ""),
26
+ amount=data["amount"],
27
+ tg_id=data.get("tg_id", 0),
28
+ comment=data.get("comment", ""),
29
+ date=dt,
30
+ )
31
+
32
+ def __str__(self) -> str:
33
+ comment_part = f' — "{self.comment}"' if self.comment else ""
34
+ return (
35
+ f"[{self.date.strftime('%Y-%m-%d %H:%M')}] "
36
+ f"{self.name} → {self.amount}₽{comment_part}"
37
+ )
38
+
39
+
40
+ @dataclass
41
+ class Card:
42
+ token: str # токен для удаления и операций
43
+ first_six: str # первые 6 цифр (BIN)
44
+ last_four: str # последние 4 цифры
45
+ card_type: str # MIR / VISA / MASTERCARD
46
+ expiration_date: str # MM/YY
47
+ issuer_code: str # название банка
48
+ is_default: bool # карта по умолчанию для выплат
49
+ commission_hint: str # текст подсказки о комиссии
50
+
51
+ @classmethod
52
+ def from_dict(cls, data: dict) -> "Card":
53
+ return cls(
54
+ token=data["token"],
55
+ first_six=data.get("firstSix", ""),
56
+ last_four=data.get("lastFour", ""),
57
+ card_type=data.get("cardType", ""),
58
+ expiration_date=data.get("cardExpirationDate", ""),
59
+ issuer_code=data.get("issuerCode", ""),
60
+ is_default=data.get("isDefault", False),
61
+ commission_hint=data.get("commissionHint", ""),
62
+ )
63
+
64
+ def __str__(self) -> str:
65
+ default = " [по умолчанию]" if self.is_default else ""
66
+ return (
67
+ f"{self.card_type} *{self.last_four} "
68
+ f"({self.issuer_code}, до {self.expiration_date}){default}"
69
+ )
70
+
71
+
72
+ @dataclass
73
+ class PayoutFeeInfo:
74
+ text: str # текст с описанием комиссий
75
+ downgrade_condition: str
76
+ tinkoff_commission_hint: str
77
+ instant_payout_commission_text: str
78
+
79
+ @classmethod
80
+ def from_dict(cls, data: dict) -> "PayoutFeeInfo":
81
+ return cls(
82
+ text=data.get("text", ""),
83
+ downgrade_condition=data.get("downGradeCondition", ""),
84
+ tinkoff_commission_hint=data.get("tinkoffCommissionHint", ""),
85
+ instant_payout_commission_text=data.get("instantPayoutCommissionText", ""),
86
+ )
87
+
88
+
89
+ @dataclass
90
+ class AccumulationSummary:
91
+ accumulated_amount: float # накоплено (ещё не выведено)
92
+ amount_to_deposit: float # к зачислению
93
+ commission: float # сумма комиссии
94
+ commission_percent: float # процент комиссии
95
+ next_payout_date: Optional[str] # дата следующей выплаты (None если не запланирована)
96
+ commission_hint: str # текст подсказки
97
+
98
+ @classmethod
99
+ def from_dict(cls, data: dict) -> "AccumulationSummary":
100
+ return cls(
101
+ accumulated_amount=data.get("accumulatedAmount", 0.0),
102
+ amount_to_deposit=data.get("amountToDeposit", 0.0),
103
+ commission=data.get("commission", 0.0),
104
+ commission_percent=data.get("commissionPercent", 0.0),
105
+ next_payout_date=data.get("nextPayoutDate"),
106
+ commission_hint=data.get("commissionHint", ""),
107
+ )
108
+
109
+
110
+ @dataclass
111
+ class ReceiverProfile:
112
+ user_id: str
113
+ full_name: str
114
+ phone_number: str
115
+ photo_url: str
116
+ payout_method: str # "Instant" или "Accumulation"
117
+ instant_payout_enabled: bool
118
+ is_premium: bool
119
+ onboarding_passed: bool
120
+ gender: str
121
+ work_place: Optional[str]
122
+ work_position: Optional[str]
123
+ birthday: Optional[str]
124
+ created_date: str
125
+ available_amount_min: float
126
+ available_amount_max: float
127
+
128
+ @classmethod
129
+ def from_dict(cls, data: dict) -> "ReceiverProfile":
130
+ available = data.get("availableAmount") or {}
131
+ return cls(
132
+ user_id=data.get("userId", ""),
133
+ full_name=data.get("fullName", ""),
134
+ phone_number=data.get("phoneNumber", ""),
135
+ photo_url=data.get("photoUrl", ""),
136
+ payout_method=data.get("payoutMethod", "Instant"),
137
+ instant_payout_enabled=data.get("instantPayoutEnabled", False),
138
+ is_premium=data.get("isPremium", False),
139
+ onboarding_passed=data.get("onboardingPassed", False),
140
+ gender=data.get("gender", "NotSpecified"),
141
+ work_place=data.get("workPlace"),
142
+ work_position=data.get("workPosition"),
143
+ birthday=data.get("birthday"),
144
+ created_date=data.get("createdDate", ""),
145
+ available_amount_min=available.get("minimal", 0.0),
146
+ available_amount_max=available.get("maximal", 0.0),
147
+ )
148
+
149
+ def __str__(self) -> str:
150
+ return (
151
+ f"{self.full_name} ({self.phone_number})\n"
152
+ f"Метод выплат: {self.payout_method} | "
153
+ f"Премиум: {'да' if self.is_premium else 'нет'} | "
154
+ f"Лимиты: {self.available_amount_min}₽ — {self.available_amount_max}₽"
155
+ )
156
+
157
+
158
+ """Новые токены, которые библиотека передаёт в on_token_refresh."""
159
+ access_token: str
160
+ refresh_token: str
161
+ expires_at: float # unix timestamp
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cloudtips"
7
- version = "0.1.1"
7
+ version = "0.3.0"
8
8
  description = "Неофициальная Python-библиотека для CloudTips (получение донатов, поллинг, обновление токенов)"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,48 +0,0 @@
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
File without changes
File without changes
File without changes