Metadata-Version: 2.5
Name: repsapi
Version: 0.1.0
Summary: Official RepsApi.pl client: parcel tracking, QC photos, product lookup, agent links and shipping estimates.
Project-URL: Homepage, https://repsapi.pl
Project-URL: Documentation, https://repsapi.pl/docs
Project-URL: Changelog, https://repsapi.pl/changelog
Project-URL: Status, https://repsapi.pl/status
Author: RepsApi.pl
License-Expression: MIT
License-File: LICENSE
Keywords: 1688,agent,api-client,qc,reps,repsapi,shipping,taobao,tracking,weidian
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.23
Description-Content-Type: text/markdown

# repsapi

Official Python client for the [RepsApi.pl](https://repsapi.pl) API: parcel tracking, warehouse QC
photos, product lookup, agent links, shipping estimates and haul calculations.

- Sync (`RepsApi`) and async (`AsyncRepsApi`) clients, e.g. for discord.py bots.
- Typed responses (`TypedDict`), so your editor completes every field.
- Retries network errors, 5xx answers and short rate limits on its own.
- One dependency: [httpx](https://www.python-httpx.org/). Python 3.9+.

## Install

```bash
pip install repsapi
```

## Quick start

Create an API key in the [dashboard](https://repsapi.pl/dashboard) and put it in the `REPSAPI_KEY`
environment variable (or pass it as `api_key`).

```python
from repsapi import RepsApi

with RepsApi() as api:  # reads REPSAPI_KEY
    parcel = api.tracking("YT2530998211074512")
    if parcel["summary"]:
        print(parcel["summary"]["status_en"])

    item = api.lookup("https://weidian.com/item.html?itemID=7779617259", currency="PLN")
    print(item["title_en"], item.get("price_local"))
```

Async:

```python
import asyncio
from repsapi import AsyncRepsApi

async def main():
    async with AsyncRepsApi() as api:
        qc = await api.qc(platform="weidian", id="7779617259")
        print(qc["total_photos"])

asyncio.run(main())
```

Responses are the API's JSON as plain dicts. Field-by-field descriptions are in the
[documentation](https://repsapi.pl/docs).

## Methods

Every method also takes `timeout=` (seconds) and `max_retries=`.

| Method | API | Key |
| --- | --- | --- |
| `tracking(number)` | `GET /v1/tracking` | yes |
| `qc(url)` / `qc(platform=, id=)` | `GET /v1/qc` | yes |
| `lookup(url, currency=, agent=)` / `lookup(platform=, id=, ...)` | `GET /v1/lookup` | yes |
| `convert(url, agents=)` | `GET /v1/convert` | yes |
| `shipping_estimate(country, weight=, dimensions=, urls=, currency=, battery=, brands=, liquids=)` | `POST /v1/shipping/estimate` | yes |
| `haul(items, country, currency=, battery=, brands=, liquids=)` | `POST /v1/haul` | yes |
| `usage()` | `GET /v1/usage` | yes |
| `shipping_countries()` | `GET /v1/shipping/countries` | no |
| `rates(base="CNY")` | `GET /v1/rates` | no |
| `health(days=)` | `GET /v1/health` | no |
| `changelog(limit=, lang=)` | `GET /v1/changelog` | no |

```python
# the same product on two agents
links = api.convert("https://detail.1688.com/offer/678091425228.html", agents=["kakobuy", "cnfans"])
print(links["agents"]["kakobuy"])

# shipping lines for a 1.2 kg parcel to Poland, priced in PLN
estimate = api.shipping_estimate("PL", weight=1200, currency="PLN")
print(estimate["cheapest"])

# a whole haul
haul = api.haul(
    [
        {"url": "https://weidian.com/item.html?itemID=7779617259", "quantity": 2},
        {"url": "https://detail.1688.com/offer/678091425228.html"},
    ],
    "PL",
    currency="PLN",
)
print(haul["totals"].get("local"))
```

## Errors

Every error extends `RepsApiError`. Errors from the API extend `APIError` and carry `status`, `type`
(the API's error type), `message` and `details` (extra fields such as `allowed`).

| Class | When |
| --- | --- |
| `BadRequestError` | 400 / 413 / 415: something in the request is wrong |
| `AuthenticationError` | 401: missing, invalid or revoked key |
| `PermissionDeniedError` | 403: the key lacks this API's scope, or the IP isn't whitelisted |
| `UnprocessableEntityError` | 422: no platform and product id in the link |
| `RateLimitError` | 429: has `retry_after` (seconds), `reset_at`, `limit`, `used`, `scope` |
| `InternalServerError` | 5xx |
| `APIConnectionError` / `APITimeoutError` | no answer at all |

```python
from repsapi import RateLimitError, UnprocessableEntityError

try:
    api.lookup(link)
except RateLimitError as err:
    print("Quota used up, resets", err.reset_at)
except UnprocessableEntityError:
    print("That isn't a product link.")
```

## Retries, timeouts and limits

- Network errors and 500/502/503/504 answers are retried twice with a growing pause (`max_retries`).
- A `429` is retried after its `Retry-After` when that is at most 60 s (`max_retry_wait`); a used-up
  daily quota raises `RateLimitError` at once.
- Failed calls don't count toward your quota, so retries cost nothing.
- Timeouts: 60 s, hauls and link-based shipping estimates 180 s (`timeout`).
- `api.rate_limit` holds the per-minute limit from the last response that carried one.
- The client never follows redirects, so your key is never sent anywhere but `base_url`, and
  `base_url` must be `https://`. `repr(api)` never shows the key.

```python
api = RepsApi(api_key="rk_live_...", timeout=30, max_retries=3)
```

## Links

- Documentation: https://repsapi.pl/docs
- Status: https://repsapi.pl/status
- Discord: https://discord.gg/repsapi

## License

MIT
