Skip to main content

Async integration layer between Home Assistant and Dutch municipal visitor parking APIs.

Project description

pyCityVisitorParking

PyPI version Python versions CI

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:

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.

pip install pycityvisitorparking

Quickstart

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:

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:

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:

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:

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:

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.

Provider development

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

A provider folder typically contains:

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

Related docs:

License

MIT. See LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pycityvisitorparking-0.5.20.tar.gz (62.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pycityvisitorparking-0.5.20-py3-none-any.whl (58.0 kB view details)

Uploaded Python 3

File details

Details for the file pycityvisitorparking-0.5.20.tar.gz.

File metadata

  • Download URL: pycityvisitorparking-0.5.20.tar.gz
  • Upload date:
  • Size: 62.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pycityvisitorparking-0.5.20.tar.gz
Algorithm Hash digest
SHA256 8e67355be6c8ec6fd0a06f168035798ee48f6634569e205429a0ab60fbcc991c
MD5 3730dd97524d2919366ac79340ab76bd
BLAKE2b-256 1c286aa77a8439263ec53303c3d2954b39fff738ab30694c6b24690879b01ffd

See more details on using hashes here.

Provenance

The following attestation bundles were made for pycityvisitorparking-0.5.20.tar.gz:

Publisher: release.yml on sir-Unknown/pyCityVisitorParking

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pycityvisitorparking-0.5.20-py3-none-any.whl.

File metadata

File hashes

Hashes for pycityvisitorparking-0.5.20-py3-none-any.whl
Algorithm Hash digest
SHA256 9d338e6241ba3d6d00f059e58142e43f9914fee67cecf40149822a18dd186c11
MD5 04b7f9e11d1071ce1b40515343785c24
BLAKE2b-256 5468d3a86fe5909efd2cb6ec880d672a02f588f265fdac06db98a7ea48a740e2

See more details on using hashes here.

Provenance

The following attestation bundles were made for pycityvisitorparking-0.5.20-py3-none-any.whl:

Publisher: release.yml on sir-Unknown/pyCityVisitorParking

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page