Metadata-Version: 2.4
Name: shelfatlas
Version: 1.0.0
Summary: Official Python client for the ShelfAtlas Catalog API.
Project-URL: Homepage, https://shelfatlas.com/docs
Project-URL: Repository, https://github.com/SlambertDK/ShelfAtlas
Author: ShelfAtlas
License-Expression: MIT
License-File: LICENSE
Keywords: api,catalog,client,denmark,prices,retail,sdk,shelfatlas
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.7
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Description-Content-Type: text/markdown

# shelfatlas

Official, typed Python client for the [ShelfAtlas](https://shelfatlas.com) Catalog
API — read access to normalised Danish retail catalog data (products, offers,
stores, chains, and price history).

## Install

```bash
pip install shelfatlas
```

Requires Python 3.10+. Depends on `httpx` and `pydantic` v2.

## Authentication

Every request is authenticated with a ShelfAtlas API key
(`Authorization: Bearer sa_live_<key>`). Create one in your ShelfAtlas dashboard and
keep it server-side.

## Quick start

```python
from shelfatlas import CatalogClient

with CatalogClient(base_url="https://api.shelfatlas.com", api_key="sa_live_…") as client:
    # Products for one brand, minus two discontinued items
    products = client.products.list(
        brand_ids=["<brand-uuid>"],
        exclude_ids=["<product-uuid>", "<product-uuid>"],
        limit=24,
    )
    for product in products.data:
        print(product.id, product.canonical_name)

    # Current offers for a batch of products
    offers = client.offers.list(product_ids=[p.id for p in products.data])

    # Daily minimum-price series
    history = client.price_history.daily(product_ids=[p.id for p in products.data])

    # Stores within 5 km of a coordinate
    stores = client.stores.nearest(lat=55.68, lng=12.57, radius_km=5)

    # All chains (not paginated)
    chains = client.chains.list()

    # Walk every page of a listing
    for product in client.iter_products(chain_slug="netto"):
        print(product.canonical_name)
```

Numeric `price` fields are decimal **strings** (Postgres `numeric` columns are
returned as strings, not floats) — parse with `decimal.Decimal` before arithmetic.

## Methods

| Group           | Method                                   | Endpoint                    |
| --------------- | ---------------------------------------- | --------------------------- |
| `products`      | `list(**filters)` → `Page[Product]`      | `GET /products`             |
|                 | `get(id)` → `Product`                    | `GET /products/:id`         |
|                 | `offers_for(id, ...)` → `Page[Offer]`    | `GET /products/:id/offers`  |
| `offers`        | `list(**filters)` → `Page[Offer]`        | `GET /offers`               |
|                 | `get(id)` → `Offer`                      | `GET /offers/:id`           |
| `stores`        | `list(...)` → `Page[Store]`              | `GET /stores`               |
|                 | `nearest(lat, lng, radius_km=...)`       | `GET /stores` (nearest)     |
|                 | `get(id)` → `Store`                      | `GET /stores/:id`           |
| `chains`        | `list()` → `list[Chain]`                 | `GET /chains`               |
| `price_history` | `daily(product_ids, days=...)`           | `GET /price-history`        |
| client          | `iter_products/iter_offers/iter_stores`  | cursor walkers (generators) |

## Errors

Every failure raises `CatalogAPIError` with a `.code`:
`unauthorized` · `rate_limited` (carries `.retry_after_sec`) · `invalid_params` ·
`not_found` · `server_error` · `network_error` · `invalid_response`. GETs retry
automatically on 429/5xx (3 attempts, exponential backoff, honouring `Retry-After`).

```python
from shelfatlas import CatalogAPIError

try:
    client.products.get("missing")
except CatalogAPIError as err:
    if err.code == "not_found":
        ...
```

## Rate limits & quotas

Free keys: 1,000 requests lifetime, 60 requests/minute.
Partner keys: no lifetime cap, 600 requests/minute.

## API reference

The full machine-readable contract this client conforms to is published as OpenAPI
3.1 at <https://shelfatlas.com/docs/openapi.json>. Human docs:
<https://shelfatlas.com/docs>.

## Development

This SDK has its own toolchain (uv), independent of the JS monorepo:

```bash
make setup     # uv venv (Python 3.12) + install -e ".[dev]"
make test      # pytest
make build     # uv build → sdist + wheel in dist/
make smoke CATALOG_API_KEY=sa_live_…   # live end-to-end test
make openapi   # regenerate tests/openapi.json from the TS source of truth
```

## License

MIT
