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 integration layer between Home Assistant and Dutch municipal visitor parking APIs.

Status

This package ships the core client, provider interface, and discovery tooling, plus the following providers:

  • DVS Portal
  • The Hague

list_providers() reads provider manifests under src/pycityvisitorparking/provider/.

Provider documentation:

Supported municipalities

For exact base_url and api_uri values, see the provider READMEs.

  • 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

Installation

Requires Python 3.13+.

pip install pycityvisitorparking

Quickstart

import asyncio

from pycityvisitorparking import Client


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


asyncio.run(main())

Usage

import asyncio

from pycityvisitorparking import Client


async def main() -> None:
    async with Client() as client:
        providers = await client.list_providers()
        print(providers)


asyncio.run(main())

Configuration

  • base_url is required for provider requests.
  • api_uri is optional; providers may define a default (see provider README).
  • Credentials are standardized: username and password are required.
  • Provider-specific optional fields (for example permit_media_type_id) are documented in each provider README.

Async behavior

Provider discovery (list_providers(), get_provider()) runs in background threads so async callers avoid blocking the event loop.

Available data

The public API exposes a small, provider-agnostic set of models and operations. Provider READMEs list credential requirements and any unsupported operations.

  • Providers: list_providers() returns ProviderInfo with id, favorite_update_fields, and reservation_update_fields.
  • Permit: get_permit() returns Permit with id, remaining_balance (minutes), and zone_validity.
  • Zone validity: each ZoneValidityBlock includes start_time and end_time (UTC ISO 8601).
  • Reservations: list_reservations(), start_reservation(), update_reservation(), and end_reservation() return Reservation with id, name, license_plate, start_time, and end_time. Some providers only support updating end_time (see reservation_update_fields).
  • Favorites: list_favorites() and add_favorite() return Favorite with id, name, and license_plate. update_favorite() returns Favorite when supported (favorite_update_fields is non-empty), otherwise it raises ProviderError. remove_favorite() removes the entry without returning data.

Examples

Providers (list_providers()):

import asyncio

from pycityvisitorparking import Client


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


asyncio.run(main())

Permit (get_permit()):

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()
        print(permit.id, permit.remaining_balance)


asyncio.run(main())

Zone validity (ZoneValidityBlock):

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()
        for block in permit.zone_validity:
            print(block.start_time, block.end_time)


asyncio.run(main())

Reservations (list_reservations(), start_reservation(), update_reservation(), end_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"})

        reservations = await provider.list_reservations()
        print([reservation.id for reservation in reservations])

        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",
        )

        if "end_time" in provider.reservation_update_fields:
            new_end_time = end_time + timedelta(hours=1)
            reservation = await provider.update_reservation(
                reservation.id,
                end_time=new_end_time,
            )
            end_time = new_end_time

        ended = await provider.end_reservation(reservation.id, end_time=end_time)
        print(ended.id, ended.start_time, ended.end_time)


asyncio.run(main())

Favorites (list_favorites(), add_favorite(), update_favorite(), remove_favorite()):

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"})

        favorites = await provider.list_favorites()
        print([favorite.id for favorite in favorites])

        favorite = await provider.add_favorite("12AB34", name="Visitor")
        try:
            favorite = await provider.update_favorite(favorite.id, name="Visitor 2")
            print(favorite.id, favorite.name, favorite.license_plate)
        except ProviderError:
            pass

        await provider.remove_favorite(favorite.id)


asyncio.run(main())

Provider framework

Providers are discovered via manifest.json files without importing provider modules. Discovery runs in a background thread to avoid blocking the event loop. To add a provider later, create:

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

Only files under src/pycityvisitorparking/provider/<provider_id>/ should change in a provider PR.

Manifest loading is cached (5-minute TTL); pass refresh=True or call clear_manifest_cache() to force a reload. If you use loader helpers directly, prefer their async_* variants to avoid blocking the event loop.

Credential inputs are standardized: pass username and password to login() for all providers. Provider READMEs list any optional fields such as permit_media_type_id.

Error handling

Public methods raise library exceptions instead of raw aiohttp errors:

  • AuthError: authentication failures (HTTP 401/403 or provider auth rejection).
  • NetworkError: network/timeout failures.
  • ValidationError: invalid inputs (timestamps, plates, missing fields).
  • ProviderError: provider responses or request failures not covered above.
  • Optional specific errors: RateLimitError, ServiceUnavailableError, NotFoundError, TimeoutError, and ConfigError for finer-grained handling.

Exceptions include a short detail string and a normalized error_code (auth_error, network_error, validation_error, provider_error). You can optionally pass a user_message that is safe to display directly. Exception messages avoid credentials and full license plates.

Example handling:

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 diagnostic events to the pycityvisitorparking logger using the standard Python logging module. Configure handlers and levels in your application to enable debugging output. Logs avoid credentials and full license plates.

Normalization rules

  • Public APIs accept only timezone-aware datetime values; naive timestamps raise ValidationError.
  • All public timestamps are normalized to UTC ISO 8601 with Z and second precision (microseconds are truncated).
  • Internally, reservation times are handled as timezone-aware UTC datetime values and serialized to strings only at provider and model boundaries.
  • License plates are normalized to uppercase A-Z0-9 without spaces/symbols.
  • Adding a favorite that already exists by license plate raises ValidationError.
  • zone_validity must include only chargeable windows.

Development

Run checks with Hatch:

hatch run lint:check
hatch run lint:format-check
hatch run test:run

Build artifacts:

hatch build
python -m twine check dist/*

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.10.tar.gz (45.2 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.10-py3-none-any.whl (46.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pycityvisitorparking-0.5.10.tar.gz
  • Upload date:
  • Size: 45.2 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.10.tar.gz
Algorithm Hash digest
SHA256 25a94f7edf634fdf694323c3a67bbed32dfdad3584c7fa218b6449a5a3d4b046
MD5 744533e37edf26f14633327ea8378ea0
BLAKE2b-256 bd91f4837b1d38de17c791fe0fa5648bdd6c3eb9f8c90839492f9387e208be4d

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for pycityvisitorparking-0.5.10-py3-none-any.whl
Algorithm Hash digest
SHA256 60bc361bed970f8f57afd0cab3935f566ed7e8890c51c04331dd9a3d12b59cb2
MD5 01aaa91d02ffaada801330c1fa2a3852
BLAKE2b-256 91067ce373a419896d3593a56f173ae0c75b100281c0ffdf3d308e5832e3d803

See more details on using hashes here.

Provenance

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