Metadata-Version: 2.4
Name: pycityvisitorparking
Version: 0.5.24
Summary: Async integration layer between Home Assistant and Dutch municipal visitor parking APIs.
Project-URL: Homepage, https://github.com/sir-Unknown/pyCityVisitorParking
Project-URL: Repository, https://github.com/sir-Unknown/pyCityVisitorParking
Project-URL: Issues, https://github.com/sir-Unknown/pyCityVisitorParking/issues
Author: pyCityVisitorParking contributors
License: MIT
License-File: LICENSE
Keywords: aiohttp,async,home-assistant,municipality,parking
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Home Automation
Requires-Python: >=3.14
Requires-Dist: aiohttp>=3.9
Description-Content-Type: text/markdown

# pyCityVisitorParking

[![PyPI version](https://img.shields.io/pypi/v/pycityvisitorparking)](https://pypi.org/project/pycityvisitorparking/)
[![Python versions](https://img.shields.io/pypi/pyversions/pycityvisitorparking)](https://pypi.org/project/pycityvisitorparking/)
[![CI](https://github.com/sir-Unknown/pyCityVisitorParking/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/sir-Unknown/pyCityVisitorParking/actions/workflows/ci.yml)

Async Python library for Dutch municipal visitor parking providers.

The library exposes a small provider-agnostic API for:
- provider discovery
- permit lookup
- reservation management
- favorites management

It is designed for Home Assistant use, but can also be used directly in any async Python application.

## Status

Current bundled providers:
- DVS Portal
- The Hague
- 2park

Provider manifests are discovered from `src/pycityvisitorparking/provider/` without importing all providers up front.

Provider documentation:
- DVS Portal: <https://github.com/sir-Unknown/pyCityVisitorParking/blob/main/src/pycityvisitorparking/provider/dvsportal/README.md>
- The Hague: <https://github.com/sir-Unknown/pyCityVisitorParking/blob/main/src/pycityvisitorparking/provider/the_hague/README.md>
- 2park: <https://github.com/sir-Unknown/pyCityVisitorParking/blob/main/src/pycityvisitorparking/provider/2park/README.md>

## Supported municipalities

For the exact `base_url`, `api_uri`, and provider-specific notes, see the provider README files.

- DVS Portal: Apeldoorn, Bloemendaal, Delft, Den Bosch, Doetinchem (via Buha), Groningen, Haarlem, Harlingen, Heemstede, Heerenveen, Heerlen, Hengelo, Katwijk, Leiden, Leidschendam-Voorburg, Middelburg, Nissewaard, Oldenzaal, Rijswijk, Roermond, Schouwen-Duiveland, Sittard-Geleen, Smallingerland, Sudwest-Fryslan, Veere, Venlo, Vlissingen, Waadhoeke, Waalwijk, Weert, Zaanstad, Zevenaar, Zutphen, Zwolle
- The Hague: The Hague
- 2park: Amstelveen, Assen, Bergen op Zoom, Breda, Deventer, Dordrecht, Eindhoven, Emmen, Etten-Leur, Gorinchem, Hardenberg, Harderwijk, Maastricht, Oosterhout, Oss, Roosendaal, Sluis, Terneuzen, Tiel, Veenendaal, Vlaardingen

The 2park list is non-exhaustive. Check <https://mijn.2park.nl> for the current provider coverage.

## Installation

Requires Python 3.14 or newer.

```bash
pip install pycityvisitorparking
```

## Quickstart

```python
import asyncio

from pycityvisitorparking import Client


async def main() -> None:
    async with Client(base_url="https://example", api_uri="/api") as client:
        provider = await client.get_provider("dvsportal")
        await provider.login(credentials={"username": "user", "password": "secret"})

        permit = await provider.get_permit()
        reservations = await provider.list_reservations()

        print(permit)
        print(reservations)


asyncio.run(main())
```

## Public API

### Client

`Client` is the main entry point.

- `list_providers()` returns available `ProviderInfo` objects.
- `get_provider(provider_id, ...)` loads a specific provider on demand.
- `Client` accepts an optional injected `aiohttp.ClientSession`.
- If you do not inject a session, the client creates and owns its own session.

Configuration options:
- `base_url`: provider base URL
- `api_uri`: optional provider API path
- `timeout`: optional `aiohttp.ClientTimeout`
- `retry_count`: retry count for idempotent GET requests

### Data models

The public data models are:
- `ProviderInfo`
- `Permit`
- `ZoneValidityBlock`
- `Reservation`
- `Favorite`

Model highlights:
- `ProviderInfo` includes `id`, `favorite_update_fields`, `reservation_update_fields`, and `balance_units`
- `Permit` includes `id`, `remaining_balance`, `balance_unit`, and `zone_validity`
- `ZoneValidityBlock` contains UTC ISO 8601 `start_time` and `end_time`
- `Reservation` includes `id`, `name`, `license_plate`, `start_time`, and `end_time`
- `Favorite` includes `id`, `name`, and `license_plate`

### Standardized behavior

- Timestamps are returned as UTC ISO 8601 strings.
- License plates are normalized and validated.
- The public API stays provider-agnostic.
- Some update operations may be unsupported by a provider; inspect `favorite_update_fields` and `reservation_update_fields`.

## Common usage

List providers:

```python
import asyncio

from pycityvisitorparking import Client


async def main() -> None:
    async with Client() as client:
        for provider in await client.list_providers():
            print(provider.id, provider.reservation_update_fields)


asyncio.run(main())
```

Manage a reservation:

```python
import asyncio
from datetime import datetime, timedelta, timezone

from pycityvisitorparking import Client


async def main() -> None:
    async with Client(base_url="https://example", api_uri="/api") as client:
        provider = await client.get_provider("dvsportal")
        await provider.login(credentials={"username": "user", "password": "secret"})

        start_time = datetime(2024, 5, 1, 9, 0, tzinfo=timezone.utc)
        end_time = start_time + timedelta(hours=2)

        reservation = await provider.start_reservation(
            "12AB34",
            start_time=start_time,
            end_time=end_time,
            name="Visitor",
        )

        print(reservation.id)


asyncio.run(main())
```

Manage favorites:

```python
import asyncio

from pycityvisitorparking import Client
from pycityvisitorparking.exceptions import ProviderError


async def main() -> None:
    async with Client(base_url="https://example", api_uri="/api") as client:
        provider = await client.get_provider("dvsportal")
        await provider.login(credentials={"username": "user", "password": "secret"})

        favorite = await provider.add_favorite("12AB34", name="Visitor")

        try:
            updated = await provider.update_favorite(favorite.id, name="Visitor 2")
            print(updated)
        except ProviderError:
            print("Favorite updates are not supported by this provider")


asyncio.run(main())
```

## Error handling

Public methods raise library exceptions instead of raw `aiohttp` exceptions:

- `AuthError`
- `NetworkError`
- `ValidationError`
- `ProviderError`
- `RateLimitError`
- `ServiceUnavailableError`
- `NotFoundError`
- `TimeoutError`
- `ConfigError`

Each exception includes normalized metadata such as `error_code`, `detail`, and optional `user_message`.

Example:

```python
from pycityvisitorparking import Client
from pycityvisitorparking.exceptions import AuthError, NetworkError, ProviderError, ValidationError

async with Client(base_url=base_url, api_uri=api_uri) as client:
    try:
        provider = await client.get_provider("dvsportal")
        await provider.login(credentials={"username": "user", "password": "secret"})
        permit = await provider.get_permit()
    except (AuthError, ValidationError) as exc:
        handle_auth_or_input_error(exc)
    except NetworkError as exc:
        handle_network_issue(exc)
    except ProviderError as exc:
        handle_provider_issue(exc)
```

## Logging

The library logs to the `pycityvisitorparking` logger using the standard Python `logging` module.

- credentials are not logged
- full license plates are not logged
- request context can be attached for clearer diagnostics

## Development

This repository uses `uv` for local development tasks.

Common checks:

```bash
uv run --group lint ruff check .
uv run --group lint ruff format --check .
uv run --group typecheck pyright
uv run --group test pytest
uv run --group schema python -m pytest -o addopts=-q tests/test_manifest_schema.py
uv build
uvx twine check dist/*
```

Release notes and publishing are handled through GitHub Actions. See [docs/RELEASING.md](docs/RELEASING.md).

## Provider development

Providers live under `src/pycityvisitorparking/provider/<provider_id>/`.

A provider folder typically contains:

```text
src/pycityvisitorparking/provider/<provider_id>/
  manifest.json
  __init__.py
  api.py
  const.py
  README.md
  CHANGELOG.md
```

Related docs:
- Provider framework: [src/pycityvisitorparking/provider/README.md](src/pycityvisitorparking/provider/README.md)
- Provider template: [docs/provider-template/README.md](docs/provider-template/README.md)
- Provider development guide: [docs/provider-development/README.md](docs/provider-development/README.md)

## License

MIT. See [LICENSE](LICENSE).
