Skip to content

Project History

Language: English | Nederlands

This page preserves the original project brief that kicked off python-appie.

It is useful as historical context because the live API evolved during implementation, which meant some of the originally assumed endpoints and payloads had to be corrected while building the package.

Original project brief

Initialize a new Python package called `python-appie` — an unofficial Python client for the Albert Heijn (Dutch supermarket) API.

## Package structure

Use `uv` for project management, `ruff` for linting/formatting, `pytest` for testing.

python-appie/
├── src/
│   └── appie/
│       ├── __init__.py
│       ├── auth.py
│       ├── client.py
│       ├── models.py
│       ├── receipts.py
│       ├── products.py
│       └── lists.py
├── tests/
│   ├── conftest.py
│   ├── test_auth.py
│   └── test_client.py
├── pyproject.toml
├── README.md
└── .github/workflows/ci.yml

## pyproject.toml

- name: `python-appie`, import name: `appie`
- Python >= 3.11
- dependencies: `httpx`, `pydantic>=2`
- dev dependencies: `pytest`, `pytest-asyncio`, `respx` (for mocking httpx), `ruff`
- ruff: line-length 100, target py311

## Auth (`auth.py`)

AH uses OAuth2. Implement an `AHAuthClient` class using `httpx.AsyncClient`.

Base URL: `https://api.ah.nl`

### Anonymous token (needed to bootstrap login)
POST /mobile-auth/v1/auth/token/anonymous
Headers:
  Content-Type: application/x-www-form-urlencoded
Body:
  client_id=appie
Returns: `{ access_token, refresh_token, expires_in }`

### User login (authorization code flow)
The login flow works as follows:
1. Open browser to: `https://login.ah.nl/secure/oauth/authorize?client_id=appie&redirect_uri=appie://login-exit&response_type=code`
2. User logs in, browser redirects to: `appie://login-exit?code=CODE`
3. Exchange code for tokens:

POST /mobile-auth/v1/auth/token
Headers:
  Content-Type: application/x-www-form-urlencoded
Body:
  client_id=appie&grant_type=authorization_code&code=CODE

### Token refresh
POST /mobile-auth/v1/auth/token
Body:
  client_id=appie&grant_type=refresh_token&refresh_token=REFRESH_TOKEN

Implement:
- `get_anonymous_token() -> TokenResponse`
- `login_with_code(code: str) -> TokenResponse`
- `refresh_token(refresh_token: str) -> TokenResponse`
- Auto-refresh logic: check expiry before each request, refresh if needed
- Persist tokens to `~/.config/appie/tokens.json` (create dir if not exists)

## Client (`client.py`)

`AHClient` — the main entry point users interact with.

async with AHClient() as client:
    await client.login()               # handles full auth flow
    receipts = await client.receipts.list()
    products = await client.products.search("melk")
    await client.lists.add_item("Halfvolle melk", quantity=2)

- Wraps `AHAuthClient`
- All requests include:
  - `Authorization: Bearer <token>`
  - `User-Agent: Appie/8.22.3`
  - `Content-Type: application/json`
- GraphQL endpoint: `POST https://api.ah.nl/graphql`
- Has a `graphql(query, variables)` helper method

## Models (`models.py`)

Pydantic v2 models:

class TokenResponse(BaseModel):
    access_token: str
    refresh_token: str
    expires_in: int
    token_type: str = "Bearer"

class Product(BaseModel):
    id: int
    title: str
    brand: str | None = None
    price: float | None = None  # in euros
    unit_size: str | None = None
    image_url: str | None = None

class ReceiptProduct(BaseModel):
    id: int
    name: str
    quantity: float
    price_per_unit: float
    total_price: float

class Receipt(BaseModel):
    id: str
    datetime: datetime
    store_name: str | None = None
    total: float
    products: list[ReceiptProduct]

class ShoppingListItem(BaseModel):
    id: str
    description: str
    quantity: int = 1
    product_id: int | None = None

## Receipts (`receipts.py`)

Use GraphQL. Based on the known schema, receipts are split into:
- **In-store receipts** (POS): `posReceipts` query → list, then `posReceiptDetails(id)` for products
- **Online orders**: via order history

Implement `ReceiptsAPI` class:
- `list_pos_receipts(limit=50) -> list[Receipt]` — in-store bonuskaart receipts
- `get_pos_receipt(receipt_id: str) -> Receipt` — with products
- `list_all(limit=50) -> list[Receipt]` — combines both, sorted by date

GraphQL queries to use:

# List in-store receipts
query PosReceipts($limit: Int!) {
  posReceipts(limit: $limit) {
    id
    transactionDate
    totalAmount { amount }
    store { name }
  }
}

# Receipt detail with products
query PosReceiptDetails($id: String!) {
  posReceiptDetails(id: $id) {
    id
    transactionDate
    totalAmount { amount }
    store { name }
    receiptLines {
      quantity
      description
      totalPrice { amount }
      unitPrice { amount }
      product { id }
    }
  }
}

## Products (`products.py`)

Use REST endpoint:
GET /mobile-services/product/search/v2?query=QUERY&sortOn=RELEVANCE&size=10&page=0

Implement `ProductsAPI`:
- `search(query: str, limit=10) -> list[Product]`
- `get(product_id: int) -> Product`

## Lists (`lists.py`)

Implement `ListsAPI`:
- `get_list() -> list[ShoppingListItem]` — fetch current shopping list
- `add_item(description: str, quantity=1, product_id=None) -> ShoppingListItem`
- `remove_item(item_id: str) -> None`
- `clear() -> None`

Note: the exact shopping list endpoint may be REST or GraphQL — use GraphQL as default and fall back gracefully. The mutation likely looks like:
mutation AddToShoppingList($input: ShoppingListItemInput!) {
  addShoppingListItem(input: $input) {
    id
    description
    quantity
  }
}

## Login CLI helper

In `__init__.py`, expose a small CLI entry point `appie-login` that:
1. Prints the AH login URL
2. Asks the user to paste the redirect URL (`appie://login-exit?code=...`)
3. Extracts the code and calls `login_with_code`
4. Saves tokens to `~/.config/appie/tokens.json`
5. Prints "✓ Logged in successfully"

## Tests

- `conftest.py`: fixture with a mocked `AHClient` using `respx`
- `test_auth.py`: test anonymous token fetch, token refresh, token persistence
- `test_client.py`: test that requests include correct headers and Bearer token

## README.md

Include:
- Brief description (unofficial AH API client)
- Install: `uv add python-appie` (or `pip install python-appie`)
- Quick start with login flow + receipts example
- Disclaimer that this is unofficial and may break
- Reference to `gwillem/appie-go` as source for endpoint discovery

Read next: Development for the local tooling, quality gates, and contribution workflow.