fazercards-python-sdk 0.1.1__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.
- fazercards_python_sdk-0.1.1.dist-info/METADATA +169 -0
- fazercards_python_sdk-0.1.1.dist-info/RECORD +18 -0
- fazercards_python_sdk-0.1.1.dist-info/WHEEL +4 -0
- fazercards_python_sdk-0.1.1.dist-info/licenses/LICENSE +21 -0
- fazercards_sdk/__init__.py +102 -0
- fazercards_sdk/_core.py +308 -0
- fazercards_sdk/_version.py +1 -0
- fazercards_sdk/client.py +58 -0
- fazercards_sdk/config.py +104 -0
- fazercards_sdk/enums.py +90 -0
- fazercards_sdk/errors.py +104 -0
- fazercards_sdk/messages.py +60 -0
- fazercards_sdk/messages.yaml +48 -0
- fazercards_sdk/models.py +515 -0
- fazercards_sdk/money.py +41 -0
- fazercards_sdk/py.typed +0 -0
- fazercards_sdk/resources.py +313 -0
- fazercards_sdk/webhooks.py +63 -0
fazercards_sdk/models.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, replace
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from typing import Any, Mapping, Optional
|
|
7
|
+
|
|
8
|
+
from .enums import OrderKind, OrderStatus
|
|
9
|
+
from .money import parse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _pick(body: Mapping[str, Any], *keys: str) -> Any:
|
|
13
|
+
for key in keys:
|
|
14
|
+
value = body.get(key)
|
|
15
|
+
if value is not None and value != "":
|
|
16
|
+
return value
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _str(body: Mapping[str, Any], *keys: str) -> Optional[str]:
|
|
21
|
+
value = _pick(body, *keys)
|
|
22
|
+
return None if value is None else str(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _int(body: Mapping[str, Any], *keys: str) -> Optional[int]:
|
|
26
|
+
value = _pick(body, *keys)
|
|
27
|
+
if value is None or isinstance(value, bool):
|
|
28
|
+
return None
|
|
29
|
+
try:
|
|
30
|
+
return int(value)
|
|
31
|
+
except (TypeError, ValueError):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _bool(body: Mapping[str, Any], *keys: str) -> Optional[bool]:
|
|
36
|
+
value = _pick(body, *keys)
|
|
37
|
+
return value if isinstance(value, bool) else None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _money(body: Mapping[str, Any], *keys: str) -> Optional[Decimal]:
|
|
41
|
+
return parse(_pick(body, *keys))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _when(body: Mapping[str, Any], *keys: str) -> Optional[datetime]:
|
|
45
|
+
value = _pick(body, *keys)
|
|
46
|
+
if not isinstance(value, str):
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
50
|
+
except ValueError:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _map(value: Any) -> Mapping[str, Any]:
|
|
55
|
+
return value if isinstance(value, Mapping) else {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _list(value: Any) -> list[Any]:
|
|
59
|
+
return list(value) if isinstance(value, (list, tuple)) else []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def card_code(card: Any) -> str:
|
|
63
|
+
if isinstance(card, Mapping):
|
|
64
|
+
parts = [str(card[key]) for key in ("code", "serial", "pin") if card.get(key) not in (None, "")]
|
|
65
|
+
return " / ".join(parts) if parts else str(dict(card))
|
|
66
|
+
return str(card)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class Balance:
|
|
71
|
+
amount: Decimal
|
|
72
|
+
currency: str
|
|
73
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_api(cls, body: Mapping[str, Any]) -> Balance:
|
|
77
|
+
return cls(_money(body, "balance") or Decimal("0"), _str(body, "currency") or "USD", dict(body))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class Account:
|
|
82
|
+
login: Optional[str]
|
|
83
|
+
email: Optional[str]
|
|
84
|
+
plan: Optional[str]
|
|
85
|
+
plan_expires_at: Optional[datetime]
|
|
86
|
+
plan_auto_renew: Optional[bool]
|
|
87
|
+
subscription_active: Optional[bool]
|
|
88
|
+
total_spent: Optional[Decimal]
|
|
89
|
+
total_orders: Optional[int]
|
|
90
|
+
created_at: Optional[datetime]
|
|
91
|
+
last_active_at: Optional[datetime]
|
|
92
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_api(cls, body: Mapping[str, Any]) -> Account:
|
|
96
|
+
summary = _map(body.get("summary"))
|
|
97
|
+
return cls(
|
|
98
|
+
login=_str(body, "login"),
|
|
99
|
+
email=_str(body, "email"),
|
|
100
|
+
plan=_str(body, "plan"),
|
|
101
|
+
plan_expires_at=_when(body, "planExpiresAt", "plan_expires_at"),
|
|
102
|
+
plan_auto_renew=_bool(body, "planAutoRenew", "plan_auto_renew"),
|
|
103
|
+
subscription_active=_bool(body, "subscriptionActive", "subscription_active"),
|
|
104
|
+
total_spent=_money(summary, "totalSpent", "total_spent"),
|
|
105
|
+
total_orders=_int(summary, "totalOrders", "total_orders"),
|
|
106
|
+
created_at=_when(body, "createdAt", "created_at"),
|
|
107
|
+
last_active_at=_when(body, "lastActiveAt", "last_active_at"),
|
|
108
|
+
raw=dict(body),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class InputField:
|
|
114
|
+
key: str
|
|
115
|
+
label: Optional[str] = None
|
|
116
|
+
type: str = "text"
|
|
117
|
+
required: bool = True
|
|
118
|
+
options: tuple[Any, ...] = ()
|
|
119
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def from_api(cls, body: Mapping[str, Any]) -> InputField:
|
|
123
|
+
required = body.get("required")
|
|
124
|
+
return cls(
|
|
125
|
+
key=_str(body, "key", "name") or "",
|
|
126
|
+
label=_str(body, "label"),
|
|
127
|
+
type=_str(body, "type") or "text",
|
|
128
|
+
required=required if isinstance(required, bool) else True,
|
|
129
|
+
options=tuple(_list(body.get("options"))),
|
|
130
|
+
raw=dict(body),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class Category:
|
|
136
|
+
category_id: str
|
|
137
|
+
name: str
|
|
138
|
+
note: Optional[str] = None
|
|
139
|
+
image_url: Optional[str] = None
|
|
140
|
+
kind: Optional[str] = None
|
|
141
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_api(cls, body: Mapping[str, Any], kind: Optional[str] = None) -> Category:
|
|
145
|
+
return cls(
|
|
146
|
+
category_id=_str(body, "category_id", "id") or "",
|
|
147
|
+
name=_str(body, "name") or "",
|
|
148
|
+
note=_str(body, "note"),
|
|
149
|
+
image_url=_str(body, "imageurl", "image_url", "image"),
|
|
150
|
+
kind=kind,
|
|
151
|
+
raw=dict(body),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class CategoryPage:
|
|
157
|
+
items: tuple[Category, ...]
|
|
158
|
+
total: Optional[int]
|
|
159
|
+
limit: Optional[int]
|
|
160
|
+
next_cursor: Optional[str]
|
|
161
|
+
has_more: bool
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def from_api(cls, body: Mapping[str, Any]) -> CategoryPage:
|
|
165
|
+
meta = _map(body.get("meta"))
|
|
166
|
+
kind = _str(body, "kind")
|
|
167
|
+
cursor = _str(meta, "next_cursor")
|
|
168
|
+
return cls(
|
|
169
|
+
items=tuple(Category.from_api(i, kind) for i in _list(body.get("items")) if isinstance(i, Mapping)),
|
|
170
|
+
total=_int(meta, "total"),
|
|
171
|
+
limit=_int(meta, "limit"),
|
|
172
|
+
next_cursor=cursor,
|
|
173
|
+
has_more=bool(meta.get("has_more")) and cursor is not None,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class Offer:
|
|
179
|
+
id: str
|
|
180
|
+
name: str
|
|
181
|
+
price: Optional[Decimal]
|
|
182
|
+
stock: Optional[int] = None
|
|
183
|
+
min_quantity: int = 1
|
|
184
|
+
max_quantity: Optional[int] = None
|
|
185
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def in_stock(self) -> bool:
|
|
189
|
+
return self.stock is None or self.stock > 0
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def from_api(cls, body: Mapping[str, Any]) -> Offer:
|
|
193
|
+
return cls(
|
|
194
|
+
id=_str(body, "offer_id", "card_id", "id") or "",
|
|
195
|
+
name=_str(body, "name") or "",
|
|
196
|
+
price=_money(body, "price_usd", "price"),
|
|
197
|
+
stock=_int(body, "stock"),
|
|
198
|
+
min_quantity=_int(body, "min_order_quantity", "min_quantity") or 1,
|
|
199
|
+
max_quantity=_int(body, "max_order_quantity", "max_quantity"),
|
|
200
|
+
raw=dict(body),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass(frozen=True)
|
|
205
|
+
class Offers:
|
|
206
|
+
category_id: str
|
|
207
|
+
name: str
|
|
208
|
+
kind: Optional[str]
|
|
209
|
+
offers: tuple[Offer, ...]
|
|
210
|
+
fields: tuple[InputField, ...] = ()
|
|
211
|
+
note: Optional[str] = None
|
|
212
|
+
image_url: Optional[str] = None
|
|
213
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
214
|
+
|
|
215
|
+
def get(self, offer_id: str) -> Optional[Offer]:
|
|
216
|
+
return next((offer for offer in self.offers if offer.id == offer_id), None)
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def from_api(cls, body: Mapping[str, Any]) -> Offers:
|
|
220
|
+
return cls(
|
|
221
|
+
category_id=_str(body, "category_id") or "",
|
|
222
|
+
name=_str(body, "name") or "",
|
|
223
|
+
kind=_str(body, "kind"),
|
|
224
|
+
offers=tuple(Offer.from_api(item) for item in _list(body.get("offers")) if isinstance(item, Mapping)),
|
|
225
|
+
fields=tuple(InputField.from_api(item) for item in _list(body.get("fields")) if isinstance(item, Mapping)),
|
|
226
|
+
note=_str(body, "note"),
|
|
227
|
+
image_url=_str(body, "imageurl", "image_url", "image"),
|
|
228
|
+
raw=dict(body),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@dataclass(frozen=True)
|
|
233
|
+
class ValidationGame:
|
|
234
|
+
category_id: str
|
|
235
|
+
name: str
|
|
236
|
+
fields: tuple[InputField, ...]
|
|
237
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def from_api(cls, body: Mapping[str, Any]) -> ValidationGame:
|
|
241
|
+
return cls(
|
|
242
|
+
category_id=_str(body, "category_id") or "",
|
|
243
|
+
name=_str(body, "name") or "",
|
|
244
|
+
fields=tuple(InputField.from_api(item) for item in _list(body.get("fields")) if isinstance(item, Mapping)),
|
|
245
|
+
raw=dict(body),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass(frozen=True)
|
|
250
|
+
class PlayerCheck:
|
|
251
|
+
valid: Optional[bool]
|
|
252
|
+
category_id: Optional[str] = None
|
|
253
|
+
player_id: Optional[str] = None
|
|
254
|
+
player_name: Optional[str] = None
|
|
255
|
+
region: Optional[str] = None
|
|
256
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def from_api(cls, body: Mapping[str, Any]) -> PlayerCheck:
|
|
260
|
+
return cls(
|
|
261
|
+
valid=_bool(body, "valid"),
|
|
262
|
+
category_id=_str(body, "category_id"),
|
|
263
|
+
player_id=_str(body, "player_id"),
|
|
264
|
+
player_name=_str(body, "player_name"),
|
|
265
|
+
region=_str(body, "region"),
|
|
266
|
+
raw=dict(body),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass(frozen=True)
|
|
271
|
+
class SteamLogin:
|
|
272
|
+
can_refill: Optional[bool]
|
|
273
|
+
unverified: bool = False
|
|
274
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def valid(self) -> Optional[bool]:
|
|
278
|
+
if self.can_refill:
|
|
279
|
+
return True
|
|
280
|
+
return None if self.unverified or self.can_refill is None else False
|
|
281
|
+
|
|
282
|
+
@classmethod
|
|
283
|
+
def from_api(cls, body: Mapping[str, Any]) -> SteamLogin:
|
|
284
|
+
return cls(_bool(body, "can_refill", "canRefill"), bool(_bool(body, "unverified")), dict(body))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@dataclass(frozen=True)
|
|
288
|
+
class SteamRates:
|
|
289
|
+
base: str
|
|
290
|
+
rates: Mapping[str, Decimal]
|
|
291
|
+
updated_at: Optional[datetime] = None
|
|
292
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
293
|
+
|
|
294
|
+
def convert(self, usd: Decimal, currency: str) -> Optional[Decimal]:
|
|
295
|
+
rate = self.rates.get(str(currency).upper())
|
|
296
|
+
return None if rate is None else usd * rate
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
def from_api(cls, body: Mapping[str, Any]) -> SteamRates:
|
|
300
|
+
rates = {str(code).upper(): value for code, raw in _map(body.get("rates")).items()
|
|
301
|
+
if (value := parse(raw)) is not None}
|
|
302
|
+
return cls(_str(body, "base") or "USD", rates, _when(body, "updated_at", "updatedAt"), dict(body))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@dataclass(frozen=True)
|
|
306
|
+
class StarsQuote:
|
|
307
|
+
price_per_star: Decimal
|
|
308
|
+
min_amount: int
|
|
309
|
+
max_amount: int
|
|
310
|
+
rates_updated_at: Optional[datetime] = None
|
|
311
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
312
|
+
|
|
313
|
+
def price(self, quantity: int) -> Decimal:
|
|
314
|
+
return self.price_per_star * quantity
|
|
315
|
+
|
|
316
|
+
@classmethod
|
|
317
|
+
def from_api(cls, body: Mapping[str, Any]) -> StarsQuote:
|
|
318
|
+
return cls(
|
|
319
|
+
price_per_star=_money(body, "price_per_star") or Decimal("0"),
|
|
320
|
+
min_amount=_int(body, "min_amount") or 50,
|
|
321
|
+
max_amount=_int(body, "max_amount") or 100000,
|
|
322
|
+
rates_updated_at=_when(body, "rates_updated_at"),
|
|
323
|
+
raw=dict(body),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclass(frozen=True)
|
|
328
|
+
class PremiumPlan:
|
|
329
|
+
months: int
|
|
330
|
+
price: Decimal
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@dataclass(frozen=True)
|
|
334
|
+
class PremiumQuote:
|
|
335
|
+
plans: tuple[PremiumPlan, ...]
|
|
336
|
+
rates_updated_at: Optional[datetime] = None
|
|
337
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
338
|
+
|
|
339
|
+
def plan(self, months: int) -> Optional[PremiumPlan]:
|
|
340
|
+
return next((plan for plan in self.plans if plan.months == months), None)
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
def from_api(cls, body: Mapping[str, Any]) -> PremiumQuote:
|
|
344
|
+
plans = []
|
|
345
|
+
for item in _list(body.get("plans")):
|
|
346
|
+
if not isinstance(item, Mapping):
|
|
347
|
+
continue
|
|
348
|
+
months, price = _int(item, "months"), _money(item, "price_usd", "price")
|
|
349
|
+
if months is not None and price is not None:
|
|
350
|
+
plans.append(PremiumPlan(months, price))
|
|
351
|
+
return cls(tuple(plans), _when(body, "rates_updated_at"), dict(body))
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@dataclass(frozen=True)
|
|
355
|
+
class StatusChange:
|
|
356
|
+
status: OrderStatus
|
|
357
|
+
at: Optional[datetime]
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@dataclass(frozen=True)
|
|
361
|
+
class Order:
|
|
362
|
+
id: str
|
|
363
|
+
kind: OrderKind
|
|
364
|
+
status: OrderStatus
|
|
365
|
+
title: Optional[str] = None
|
|
366
|
+
category_id: Optional[str] = None
|
|
367
|
+
category_name: Optional[str] = None
|
|
368
|
+
offer_id: Optional[str] = None
|
|
369
|
+
offer_name: Optional[str] = None
|
|
370
|
+
quantity: Optional[int] = None
|
|
371
|
+
fields: Mapping[str, Any] = field(default_factory=dict)
|
|
372
|
+
unit_price: Optional[Decimal] = None
|
|
373
|
+
total: Optional[Decimal] = None
|
|
374
|
+
codes: tuple[str, ...] = ()
|
|
375
|
+
fail_reason: Optional[str] = None
|
|
376
|
+
history: tuple[StatusChange, ...] = ()
|
|
377
|
+
created_at: Optional[datetime] = None
|
|
378
|
+
completed_at: Optional[datetime] = None
|
|
379
|
+
idempotency_key: Optional[str] = None
|
|
380
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def delivered(self) -> bool:
|
|
384
|
+
return self.status.delivered
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def failed(self) -> bool:
|
|
388
|
+
return self.status.failed
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def in_flight(self) -> bool:
|
|
392
|
+
return self.status.in_flight
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def awaiting_codes(self) -> bool:
|
|
396
|
+
return self.kind is OrderKind.GIFT_CARD and self.delivered and not self.codes
|
|
397
|
+
|
|
398
|
+
def with_key(self, key: Optional[str]) -> Order:
|
|
399
|
+
return replace(self, idempotency_key=key)
|
|
400
|
+
|
|
401
|
+
@classmethod
|
|
402
|
+
def from_api(cls, body: Mapping[str, Any]) -> Order:
|
|
403
|
+
order = _map(body.get("order")) or body
|
|
404
|
+
history = tuple(
|
|
405
|
+
StatusChange(OrderStatus.of(item.get("status")), _when(item, "at"))
|
|
406
|
+
for item in _list(_pick(order, "status_history", "statusHistory"))
|
|
407
|
+
if isinstance(item, Mapping)
|
|
408
|
+
)
|
|
409
|
+
return cls(
|
|
410
|
+
id=_str(order, "id", "order_id") or "",
|
|
411
|
+
kind=OrderKind.of(order.get("kind")),
|
|
412
|
+
status=OrderStatus.of(order.get("status")),
|
|
413
|
+
title=_str(order, "title"),
|
|
414
|
+
category_id=_str(order, "category_id"),
|
|
415
|
+
category_name=_str(order, "category_name"),
|
|
416
|
+
offer_id=_str(order, "offer_id", "card_id"),
|
|
417
|
+
offer_name=_str(order, "offer_name"),
|
|
418
|
+
quantity=_int(order, "quantity", "stars_quantity", "starsQuantity", "months"),
|
|
419
|
+
fields=dict(_map(order.get("fields"))),
|
|
420
|
+
unit_price=_money(order, "price_usd_per_unit", "price_usd"),
|
|
421
|
+
total=_money(order, "total_usd", "chargedUsd", "charged_usd", "amountUsd"),
|
|
422
|
+
codes=tuple(card_code(card) for card in _list(order.get("cards")) if card not in (None, "")),
|
|
423
|
+
fail_reason=_str(order, "fail_reason", "failReason"),
|
|
424
|
+
history=history,
|
|
425
|
+
created_at=_when(order, "created_at", "createdAt"),
|
|
426
|
+
completed_at=_when(order, "completed_at", "completedAt"),
|
|
427
|
+
raw=dict(order),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@dataclass(frozen=True)
|
|
432
|
+
class OrderPage:
|
|
433
|
+
items: tuple[Order, ...]
|
|
434
|
+
total: Optional[int]
|
|
435
|
+
page: int
|
|
436
|
+
limit: int
|
|
437
|
+
|
|
438
|
+
@property
|
|
439
|
+
def has_more(self) -> bool:
|
|
440
|
+
return self.total is not None and self.page * self.limit < self.total
|
|
441
|
+
|
|
442
|
+
@classmethod
|
|
443
|
+
def from_api(cls, body: Mapping[str, Any]) -> OrderPage:
|
|
444
|
+
return cls(
|
|
445
|
+
items=tuple(Order.from_api(item) for item in _list(body.get("items")) if isinstance(item, Mapping)),
|
|
446
|
+
total=_int(body, "total"),
|
|
447
|
+
page=_int(body, "page") or 1,
|
|
448
|
+
limit=_int(body, "limit") or 20,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@dataclass(frozen=True)
|
|
453
|
+
class Webhook:
|
|
454
|
+
url: Optional[str]
|
|
455
|
+
enabled: bool
|
|
456
|
+
consecutive_failures: int = 0
|
|
457
|
+
last_success_at: Optional[datetime] = None
|
|
458
|
+
last_failure_at: Optional[datetime] = None
|
|
459
|
+
last_failure_reason: Optional[str] = None
|
|
460
|
+
created_at: Optional[datetime] = None
|
|
461
|
+
updated_at: Optional[datetime] = None
|
|
462
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
463
|
+
|
|
464
|
+
@classmethod
|
|
465
|
+
def from_api(cls, body: Mapping[str, Any]) -> Webhook:
|
|
466
|
+
hook = _map(body.get("webhook")) or body
|
|
467
|
+
return cls(
|
|
468
|
+
url=_str(hook, "url"),
|
|
469
|
+
enabled=bool(_bool(hook, "enabled")),
|
|
470
|
+
consecutive_failures=_int(hook, "consecutive_failures") or 0,
|
|
471
|
+
last_success_at=_when(hook, "last_success_at"),
|
|
472
|
+
last_failure_at=_when(hook, "last_failure_at"),
|
|
473
|
+
last_failure_reason=_str(hook, "last_failure_reason"),
|
|
474
|
+
created_at=_when(hook, "created_at"),
|
|
475
|
+
updated_at=_when(hook, "updated_at"),
|
|
476
|
+
raw=dict(hook),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@dataclass(frozen=True)
|
|
481
|
+
class WebhookEvent:
|
|
482
|
+
order_id: Optional[str]
|
|
483
|
+
status: OrderStatus
|
|
484
|
+
order: Optional[Order]
|
|
485
|
+
merchant_txn_id: Optional[str] = None
|
|
486
|
+
event: Optional[str] = None
|
|
487
|
+
raw: Mapping[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def needs_lookup(self) -> bool:
|
|
491
|
+
return self.order_id is not None and (self.status is OrderStatus.UNKNOWN or self.order is None)
|
|
492
|
+
|
|
493
|
+
@classmethod
|
|
494
|
+
def from_api(cls, body: Mapping[str, Any]) -> WebhookEvent:
|
|
495
|
+
order = _map(body.get("order")) or _map(body.get("data")) or body
|
|
496
|
+
if isinstance(order.get("order"), Mapping):
|
|
497
|
+
order = order["order"]
|
|
498
|
+
order_id = _str(order, "id", "order_id") or _str(body, "order_id", "id")
|
|
499
|
+
status = OrderStatus.of(_pick(order, "status") or body.get("status"))
|
|
500
|
+
parsed = Order.from_api(order) if _str(order, "id", "order_id") and _pick(order, "status") else None
|
|
501
|
+
return cls(
|
|
502
|
+
order_id=order_id,
|
|
503
|
+
status=status,
|
|
504
|
+
order=parsed,
|
|
505
|
+
merchant_txn_id=_str(order, "merchant_txn_id") or _str(body, "merchant_txn_id"),
|
|
506
|
+
event=_str(body, "event", "type"),
|
|
507
|
+
raw=dict(body),
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@dataclass(frozen=True)
|
|
512
|
+
class RateLimit:
|
|
513
|
+
limit: Optional[int]
|
|
514
|
+
remaining: Optional[int]
|
|
515
|
+
reset: Optional[float]
|
fazercards_sdk/money.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal, InvalidOperation
|
|
4
|
+
from typing import Any, Optional, Union
|
|
5
|
+
|
|
6
|
+
from .errors import InvalidRequestError
|
|
7
|
+
from .messages import text
|
|
8
|
+
|
|
9
|
+
Amount = Union[Decimal, int, float, str]
|
|
10
|
+
|
|
11
|
+
CENT = Decimal("0.01")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse(value: Any) -> Optional[Decimal]:
|
|
15
|
+
if value is None or value == "" or isinstance(value, bool):
|
|
16
|
+
return None
|
|
17
|
+
try:
|
|
18
|
+
result = Decimal(str(value))
|
|
19
|
+
except (InvalidOperation, ValueError):
|
|
20
|
+
return None
|
|
21
|
+
return result if result.is_finite() else None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def amount(value: Amount, field: str = "amount") -> Decimal:
|
|
25
|
+
if isinstance(value, bool) or not isinstance(value, (Decimal, int, float, str)):
|
|
26
|
+
raise InvalidRequestError(text("value.amount_type", field=field, value=value))
|
|
27
|
+
try:
|
|
28
|
+
result = Decimal(str(value).strip())
|
|
29
|
+
except (InvalidOperation, ValueError):
|
|
30
|
+
raise InvalidRequestError(text("value.amount_type", field=field, value=value)) from None
|
|
31
|
+
if not result.is_finite():
|
|
32
|
+
raise InvalidRequestError(text("value.amount_finite", field=field, value=value))
|
|
33
|
+
if result <= 0:
|
|
34
|
+
raise InvalidRequestError(text("value.amount_positive", field=field, value=value))
|
|
35
|
+
if result != result.quantize(CENT):
|
|
36
|
+
raise InvalidRequestError(text("value.amount_scale", field=field, value=value))
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def wire(value: Decimal) -> str:
|
|
41
|
+
return format(value.quantize(CENT), "f")
|
fazercards_sdk/py.typed
ADDED
|
File without changes
|