repsapi 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.
repsapi/_errors.py ADDED
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, Optional
5
+
6
+ __all__ = [
7
+ "RepsApiError",
8
+ "APIError",
9
+ "BadRequestError",
10
+ "AuthenticationError",
11
+ "PermissionDeniedError",
12
+ "NotFoundError",
13
+ "UnprocessableEntityError",
14
+ "RateLimitError",
15
+ "InternalServerError",
16
+ "APIConnectionError",
17
+ "APITimeoutError",
18
+ ]
19
+
20
+
21
+ class RepsApiError(Exception):
22
+ """Base class of every error this client raises."""
23
+
24
+ def __init__(self, message: str) -> None:
25
+ super().__init__(message)
26
+ self.message = message
27
+
28
+
29
+ class APIError(RepsApiError):
30
+ """The API answered with an error status."""
31
+
32
+ def __init__(
33
+ self,
34
+ status: int,
35
+ type: str,
36
+ message: str,
37
+ details: Optional[Dict[str, Any]] = None,
38
+ body: Any = None,
39
+ ) -> None:
40
+ super().__init__(message)
41
+ #: HTTP status
42
+ self.status = status
43
+ #: the API's error type, e.g. "bad_request", "unauthorized", "rate_limited"
44
+ self.type = type
45
+ #: extra fields of the error, e.g. ``allowed``, ``fields``, ``reset_at``
46
+ self.details: Dict[str, Any] = details or {}
47
+ #: the parsed response body (or the raw text when it isn't JSON)
48
+ self.body = body
49
+
50
+ def __repr__(self) -> str:
51
+ return f"{self.__class__.__name__}(status={self.status}, type={self.type!r}, message={self.message!r})"
52
+
53
+
54
+ class BadRequestError(APIError):
55
+ """400, 413, 415 and other 4xx: the request itself is wrong."""
56
+
57
+
58
+ class AuthenticationError(APIError):
59
+ """401: missing, malformed, invalid or revoked API key."""
60
+
61
+
62
+ class PermissionDeniedError(APIError):
63
+ """403: the key lacks this API's scope, or the IP isn't whitelisted."""
64
+
65
+
66
+ class NotFoundError(APIError):
67
+ """404"""
68
+
69
+
70
+ class UnprocessableEntityError(APIError):
71
+ """422: no platform and product id could be read from the link."""
72
+
73
+
74
+ class RateLimitError(APIError):
75
+ """429: per-minute rate limit or daily quota."""
76
+
77
+ def __init__(
78
+ self,
79
+ status: int,
80
+ type: str,
81
+ message: str,
82
+ details: Optional[Dict[str, Any]] = None,
83
+ body: Any = None,
84
+ retry_after: Optional[float] = None,
85
+ ) -> None:
86
+ super().__init__(status, type, message, details, body)
87
+ d = self.details
88
+ #: seconds to wait, from Retry-After
89
+ self.retry_after = retry_after
90
+ #: when the exhausted limit resets
91
+ self.reset_at: Optional[datetime] = _parse_time(d.get("reset_at"))
92
+ #: "org" = the workspace quota, "key" = the project's own daily cap
93
+ self.scope: Optional[str] = d.get("scope") if isinstance(d.get("scope"), str) else None
94
+ self.limit: Optional[int] = d.get("limit") if isinstance(d.get("limit"), int) else None
95
+ self.used: Optional[int] = d.get("used") if isinstance(d.get("used"), int) else None
96
+
97
+
98
+ class InternalServerError(APIError):
99
+ """5xx: something failed on our side (already retried)."""
100
+
101
+
102
+ class APIConnectionError(RepsApiError):
103
+ """The request never got an answer (DNS, TLS, connection reset ...)."""
104
+
105
+
106
+ class APITimeoutError(APIConnectionError):
107
+ """No answer within the timeout."""
108
+
109
+
110
+ def _parse_time(value: Any) -> Optional[datetime]:
111
+ if not isinstance(value, str):
112
+ return None
113
+ try:
114
+ # fromisoformat before 3.11 doesn't take a trailing "Z"
115
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
116
+ except ValueError:
117
+ return None
repsapi/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
repsapi/py.typed ADDED
File without changes
repsapi/types.py ADDED
@@ -0,0 +1,509 @@
1
+ """Response types of the RepsApi.pl API.
2
+
3
+ Every method returns the API's JSON as plain dicts; these TypedDicts only
4
+ describe them for editors and type checkers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Dict, List, Literal, Optional, TypedDict
10
+
11
+ Platform = Literal["weidian", "taobao", "1688", "tmall"]
12
+
13
+ Agent = Literal[
14
+ "kakobuy", "cnfans", "acbuy", "mulebuy", "hoobuy", "oopbuy", "cnshopper", "ootdbuy",
15
+ "ponybuy", "superbuy", "sugargoo", "loongbuy", "cssbuy", "usfans", "litbuy",
16
+ ]
17
+
18
+ TrackingStage = Literal[
19
+ "info_received", "received", "in_transit", "export_customs", "awaiting_flight",
20
+ "flight_departed", "flight_arrived", "import_customs", "last_mile", "out_for_delivery",
21
+ "ready_for_pickup", "delivered", "delayed", "exception", "returned",
22
+ ]
23
+
24
+ ServiceName = Literal["api", "tracking", "qc", "lookup", "convert"]
25
+ ServiceStatus = Literal["operational", "degraded", "down"]
26
+ ChangeTag = Literal["new", "improved", "fixed", "removed"]
27
+
28
+ # "from" is a keyword, hence the functional syntax
29
+ Exchange = TypedDict(
30
+ "Exchange",
31
+ {"from": str, "to": str, "rate": float, "date": str, "source": Literal["NBP", "ECB"]},
32
+ )
33
+
34
+
35
+ class Dimensions(TypedDict):
36
+ length: float
37
+ width: float
38
+ height: float
39
+
40
+
41
+ class Country(TypedDict):
42
+ code: str
43
+ name: str
44
+
45
+
46
+ # ------------------------------- tracking -------------------------------
47
+
48
+ class TrackingUpdate(TypedDict):
49
+ date: str
50
+ hour: str
51
+ status: str
52
+ status_en: str
53
+ status_original: str
54
+ stage: TrackingStage
55
+ location: str
56
+ country: Optional[str]
57
+
58
+
59
+ EstimatedDelivery = TypedDict(
60
+ "EstimatedDelivery",
61
+ {"from": str, "to": str, "based_on": int, "from_stage": TrackingStage, "slower_than_usual": bool},
62
+ )
63
+
64
+
65
+ class TrackingSummary(TypedDict):
66
+ stage: TrackingStage
67
+ status: str
68
+ status_en: str
69
+ delivered: bool
70
+ last_update: str
71
+ origin_country: Optional[str]
72
+ destination_country: Optional[str]
73
+ carrier: Optional[str]
74
+ carrier_tracking_number: Optional[str]
75
+ estimated_delivery: Optional[EstimatedDelivery]
76
+
77
+
78
+ class TrackingResponse(TypedDict):
79
+ tracking_number: str
80
+ count: int
81
+ summary: Optional[TrackingSummary]
82
+ updates: List[TrackingUpdate]
83
+ source: str
84
+ attribution: str
85
+ docs: str
86
+
87
+
88
+ # ---------------------------------- QC ----------------------------------
89
+
90
+ class _QcBatchBase(TypedDict):
91
+ date: Optional[str]
92
+ photos: List[str]
93
+
94
+
95
+ class QcBatch(_QcBatchBase, total=False):
96
+ sku: str
97
+
98
+
99
+ class _QcResponseBase(TypedDict):
100
+ platform: Platform
101
+ product_id: str
102
+ canonical: Optional[str]
103
+ provider: Optional[str]
104
+ total_photos: int
105
+ batch_count: int
106
+ batches: List[QcBatch]
107
+ listing_photos: List[str]
108
+ source: str
109
+ docs: str
110
+
111
+
112
+ class QcResponse(_QcResponseBase, total=False):
113
+ note: str
114
+
115
+
116
+ # -------------------------------- lookup --------------------------------
117
+
118
+ class PriceRange(TypedDict):
119
+ low: Optional[float]
120
+ high: Optional[float]
121
+ discount_low: Optional[float]
122
+ discount_high: Optional[float]
123
+ currency: str
124
+
125
+
126
+ class _VariantOptionBase(TypedDict):
127
+ name: str
128
+ values: List[str]
129
+
130
+
131
+ class VariantOption(_VariantOptionBase, total=False):
132
+ name_cn: str
133
+ values_en: List[str]
134
+ values_pl: List[str]
135
+ images: Dict[str, str]
136
+
137
+
138
+ class _VariantSkuBase(TypedDict):
139
+ attrs: Dict[str, str]
140
+ stock: Optional[int]
141
+ price: Optional[float]
142
+ image: Optional[str]
143
+
144
+
145
+ class VariantSku(_VariantSkuBase, total=False):
146
+ price_local: Optional[float]
147
+
148
+
149
+ class Variants(TypedDict):
150
+ options: List[VariantOption]
151
+ skus: List[VariantSku]
152
+
153
+
154
+ class Labeled(TypedDict):
155
+ label: str
156
+ value: str
157
+
158
+
159
+ class WarehouseStats(TypedDict):
160
+ storage_time: Optional[Labeled]
161
+ turnover: Optional[Labeled]
162
+ weight_g: Optional[float]
163
+ volume_cm3: Optional[float]
164
+ dimensions_cm: Optional[str]
165
+
166
+
167
+ class WeightEstimate(TypedDict):
168
+ grams: int
169
+ category: str
170
+ source: Literal["similar_items", "typical"]
171
+ based_on: Optional[int]
172
+
173
+
174
+ class PricePoint(TypedDict):
175
+ date: str
176
+ low: Optional[float]
177
+ high: Optional[float]
178
+
179
+
180
+ class Seller(TypedDict):
181
+ id: Optional[str]
182
+ name: str
183
+
184
+
185
+ class Availability(TypedDict):
186
+ in_stock: Optional[bool]
187
+ total_stock: Optional[int]
188
+
189
+
190
+ class Money(TypedDict):
191
+ price: Optional[float]
192
+ currency: str
193
+
194
+
195
+ class QcCount(TypedDict):
196
+ batches: int
197
+ photos: int
198
+
199
+
200
+ class _LookupResponseBase(TypedDict):
201
+ platform: Platform
202
+ product_id: str
203
+ canonical: Optional[str]
204
+ source_agent: Optional[Agent]
205
+ title: Optional[str]
206
+ title_en: Optional[str]
207
+ title_pl: Optional[str]
208
+ main_image: Optional[str]
209
+ images: List[str]
210
+ price: Optional[PriceRange]
211
+ domestic_shipping: Optional[Money]
212
+ min_order: Optional[int]
213
+ seller: Optional[Seller]
214
+ availability: Availability
215
+ variant_source: Optional[Platform]
216
+ sizes: List[str]
217
+ versions: List[str]
218
+ variants: Variants
219
+ warehouse: Optional[WarehouseStats]
220
+ category: Optional[str]
221
+ weight_estimate: Optional[WeightEstimate]
222
+ qc: Optional[QcCount]
223
+ price_history: List[PricePoint]
224
+ source: str
225
+ docs: str
226
+
227
+
228
+ class LookupResponse(_LookupResponseBase, total=False):
229
+ # only with currency=...
230
+ exchange: Optional[Exchange]
231
+ price_local: Optional[PriceRange]
232
+ domestic_shipping_local: Optional[Money]
233
+ note: str
234
+
235
+
236
+ # -------------------------------- convert -------------------------------
237
+
238
+ class ConvertResponse(TypedDict):
239
+ platform: Platform
240
+ id: str
241
+ canonical: str
242
+ agents: Dict[str, str]
243
+ source: str
244
+ docs: str
245
+
246
+
247
+ # ------------------------------- shipping -------------------------------
248
+
249
+ class LineDays(TypedDict):
250
+ min: Optional[int]
251
+ max: Optional[int]
252
+
253
+
254
+ class LineFees(TypedDict):
255
+ first: Optional[float]
256
+ additional: Optional[float]
257
+ service: Optional[float]
258
+ fuel: Optional[float]
259
+ handling: Optional[float]
260
+
261
+
262
+ class WeightLimits(TypedDict):
263
+ min: Optional[int]
264
+ max: Optional[int]
265
+
266
+
267
+ class LineAccepts(TypedDict):
268
+ brands: bool
269
+ battery: bool
270
+ liquids: bool
271
+
272
+
273
+ class _ShippingLineBase(TypedDict):
274
+ id: str
275
+ name: str
276
+ days: LineDays
277
+ price: float
278
+ currency: Literal["CNY"]
279
+ fees: LineFees
280
+ chargeable_weight_g: Optional[int]
281
+ volumetric_divisor: Optional[int]
282
+ weight_limits_g: WeightLimits
283
+ accepts: LineAccepts
284
+ restrictions: Optional[str]
285
+ features: Optional[str]
286
+
287
+
288
+ class ShippingLine(_ShippingLineBase, total=False):
289
+ price_local: Optional[float]
290
+
291
+
292
+ class _HaulItemBase(TypedDict):
293
+ url: str
294
+ platform: Optional[str]
295
+ product_id: Optional[str]
296
+ title: Optional[str]
297
+ title_pl: Optional[str]
298
+ image: Optional[str]
299
+ seller: Optional[str]
300
+ quantity: int
301
+ unit_price: Optional[float]
302
+ domestic_shipping: Optional[float]
303
+ weight_g: Optional[float]
304
+ weight_source: Optional[Literal["measured", "estimate"]]
305
+ dimensions_cm: Optional[Dimensions]
306
+
307
+
308
+ class HaulItem(_HaulItemBase, total=False):
309
+ unit_price_local: Optional[float]
310
+ error: Literal["unsupported_link", "not_found"]
311
+
312
+
313
+ class ParcelInfo(TypedDict):
314
+ weight_g: int
315
+ dimensions_cm: Optional[Dimensions]
316
+
317
+
318
+ class _ShippingEstimateBase(TypedDict):
319
+ country: Country
320
+ parcel: ParcelInfo
321
+ currency: Literal["CNY"]
322
+ lines: List[ShippingLine]
323
+ cheapest: Optional[str]
324
+ fastest: Optional[str]
325
+ note: str
326
+ source: str
327
+ docs: str
328
+
329
+
330
+ class ShippingEstimateResponse(_ShippingEstimateBase, total=False):
331
+ items: List[HaulItem]
332
+ exchange: Optional[Exchange]
333
+
334
+
335
+ class ShippingCountriesResponse(TypedDict):
336
+ countries: List[Country]
337
+ count: int
338
+ source: str
339
+ docs: str
340
+
341
+
342
+ # --------------------------------- haul ---------------------------------
343
+
344
+ class _HaulItemInputBase(TypedDict):
345
+ url: str
346
+
347
+
348
+ class HaulItemInput(_HaulItemInputBase, total=False):
349
+ quantity: int
350
+
351
+
352
+ class LocalTotals(TypedDict):
353
+ goods: Optional[float]
354
+ domestic_shipping: Optional[float]
355
+ shipping: Optional[float]
356
+ total: Optional[float]
357
+ currency: str
358
+
359
+
360
+ class _HaulTotalsBase(TypedDict):
361
+ goods: float
362
+ domestic_shipping: float
363
+ shipping: Optional[float]
364
+ total: Optional[float]
365
+
366
+
367
+ class HaulTotals(_HaulTotalsBase, total=False):
368
+ local: LocalTotals
369
+
370
+
371
+ class HaulParcel(TypedDict):
372
+ weight_g: int
373
+ dimensions_cm: Optional[Dimensions]
374
+ weight_complete: bool
375
+
376
+
377
+ class HaulShipping(TypedDict):
378
+ lines: List[ShippingLine]
379
+ cheapest: Optional[str]
380
+ fastest: Optional[str]
381
+
382
+
383
+ class _HaulResponseBase(TypedDict):
384
+ country: Country
385
+ currency: Literal["CNY"]
386
+ items: List[HaulItem]
387
+ totals: HaulTotals
388
+ parcel: HaulParcel
389
+ shipping: Optional[HaulShipping]
390
+ notes: List[str]
391
+ source: str
392
+ docs: str
393
+
394
+
395
+ class HaulResponse(_HaulResponseBase, total=False):
396
+ exchange: Exchange
397
+
398
+
399
+ # ----------------------------- account / misc ---------------------------
400
+
401
+ class UsageProject(TypedDict):
402
+ name: str
403
+ today: int
404
+ daily_cap: Optional[int]
405
+
406
+
407
+ class UsageWorkspace(TypedDict):
408
+ used_today: int
409
+ limit_per_day: Optional[int]
410
+ remaining: Optional[int]
411
+ rate_per_min: Optional[int]
412
+ reset_at: str
413
+
414
+
415
+ class UsageResponse(TypedDict):
416
+ plan: Literal["free", "pro", "enterprise"]
417
+ project: UsageProject
418
+ workspace: UsageWorkspace
419
+ source: str
420
+ docs: str
421
+
422
+
423
+ class RatesResponse(TypedDict):
424
+ base: str
425
+ date: str
426
+ source: Literal["NBP", "ECB"]
427
+ rates: Dict[str, float]
428
+ source_site: str
429
+ docs: str
430
+
431
+
432
+ class ServiceSources(TypedDict):
433
+ up: int
434
+ total: int
435
+
436
+
437
+ class ServiceHealth(TypedDict):
438
+ status: ServiceStatus
439
+ latency_ms: int
440
+ sources: ServiceSources
441
+
442
+
443
+ class DayService(TypedDict):
444
+ status: ServiceStatus
445
+ uptime: float
446
+
447
+
448
+ class DayHistory(TypedDict):
449
+ date: str
450
+ services: Dict[str, DayService]
451
+
452
+
453
+ class _HealthResponseBase(TypedDict):
454
+ status: ServiceStatus
455
+ checked_at: str
456
+ services: Dict[str, ServiceHealth]
457
+ uptime_30d: Dict[str, Optional[float]]
458
+ source: str
459
+ docs: str
460
+
461
+
462
+ class HealthResponse(_HealthResponseBase, total=False):
463
+ history: List[DayHistory]
464
+
465
+
466
+ class Bilingual(TypedDict):
467
+ en: str
468
+ pl: str
469
+
470
+
471
+ class ChangeItem(TypedDict):
472
+ tag: ChangeTag
473
+ en: str
474
+ pl: str
475
+
476
+
477
+ class Release(TypedDict):
478
+ version: str
479
+ date: str
480
+ url: str
481
+ title: Bilingual
482
+ items: List[ChangeItem]
483
+
484
+
485
+ class ChangelogResponse(TypedDict):
486
+ latest: Optional[str]
487
+ releases: List[Release]
488
+ rss: str
489
+ source: str
490
+
491
+
492
+ class LocalizedChangeItem(TypedDict):
493
+ tag: ChangeTag
494
+ text: str
495
+
496
+
497
+ class LocalizedRelease(TypedDict):
498
+ version: str
499
+ date: str
500
+ url: str
501
+ title: str
502
+ items: List[LocalizedChangeItem]
503
+
504
+
505
+ class LocalizedChangelogResponse(TypedDict):
506
+ latest: Optional[str]
507
+ releases: List[LocalizedRelease]
508
+ rss: str
509
+ source: str