Skip to main content

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

Project description

pyCityVisitorParking
Async Python library for Dutch municipal visitor parking providers

Provider-agnostic async Python library for Dutch municipal visitor parking systems.
Designed for Home Assistant, but usable in any async Python application.

PyPI version Python versions CI

[!TIP] Looking for the Home Assistant integration? See: ha_City-Visitor-Parking


About this library

pyCityVisitorParking is an async Python library for Dutch municipal visitor parking providers.

It exposes a small provider-agnostic API for:

  • provider discovery
  • permit lookup
  • reservation management
  • favorites management

The library is designed primarily for Home Assistant use, but it can also be used directly in any async Python application.


Status

Currently bundled providers:

  • DVS Portal
  • The Hague
  • 2park

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

Provider-specific 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

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

Public data models:

  • 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 documentation:


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.26.tar.gz (79.6 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.26-py3-none-any.whl (72.4 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for pycityvisitorparking-0.5.26.tar.gz
Algorithm Hash digest
SHA256 63997f83305826c7975f22da5142be1d242fbff9557a89770aed6dae9d5006b4
MD5 81097941508c9534be4d6b889dcbfa38
BLAKE2b-256 cf388bdca0fdfbb041e2d9d49eb3b278aa6f9eb963df3d09ddd9573b366af353

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for pycityvisitorparking-0.5.26-py3-none-any.whl
Algorithm Hash digest
SHA256 519d9d78a6d6f7f738f3dee3c566747983c8353c8fef880111fd85004b8b3e05
MD5 07b1d68e3b6882cd772a3e711e05a717
BLAKE2b-256 a3b36901bc4ffe459c3a25fca8a53131ff037579a233b4bfed348cecaa90c477

See more details on using hashes here.

Provenance

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