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.21.tar.gz (62.8 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.21-py3-none-any.whl (58.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pycityvisitorparking-0.5.21.tar.gz
  • Upload date:
  • Size: 62.8 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.21.tar.gz
Algorithm Hash digest
SHA256 848909b17ecc44592f84f15db51e512b3a9017dacc9f11816b116e3b1be0c23d
MD5 814aa1edd1bcba4d720e961b358e5b47
BLAKE2b-256 6a552b2ceb8cd96eaff14141726455350d6249e3151a34fe6f46906cdad6d01b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pycityvisitorparking-0.5.21.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.21-py3-none-any.whl.

File metadata

File hashes

Hashes for pycityvisitorparking-0.5.21-py3-none-any.whl
Algorithm Hash digest
SHA256 33cfaf7281474f36bd677b516650098b68ba658469c2cc2b078a1682166d8010
MD5 42fb31e3001acd62f5f07c4a516f028f
BLAKE2b-256 8398481a52122724f3de497b145f27c6066f306076ad505c85a7820dd8c5b4db

See more details on using hashes here.

Provenance

The following attestation bundles were made for pycityvisitorparking-0.5.21-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