Async integration layer between Home Assistant and Dutch municipal visitor parking APIs.
Project description
pyCityVisitorParking
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.
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 availableProviderInfoobjects.get_provider(provider_id, ...)loads a specific provider on demand.Clientaccepts an optional injectedaiohttp.ClientSession.- If you do not inject a session, the client creates and owns its own session.
Configuration options:
base_url: provider base URLapi_uri: optional provider API pathtimeout: optionalaiohttp.ClientTimeoutretry_count: retry count for idempotent GET requests
Data models
The public data models are:
ProviderInfoPermitZoneValidityBlockReservationFavorite
Model highlights:
ProviderInfoincludesid,favorite_update_fields,reservation_update_fields, andbalance_unitsPermitincludesid,remaining_balance,balance_unit, andzone_validityZoneValidityBlockcontains UTC ISO 8601start_timeandend_timeReservationincludesid,name,license_plate,start_time, andend_timeFavoriteincludesid,name, andlicense_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_fieldsandreservation_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:
AuthErrorNetworkErrorValidationErrorProviderErrorRateLimitErrorServiceUnavailableErrorNotFoundErrorTimeoutErrorConfigError
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:
- Provider framework: src/pycityvisitorparking/provider/README.md
- Provider template: docs/provider-template/README.md
- Provider development guide: docs/provider-development/README.md
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pycityvisitorparking-0.5.23.tar.gz.
File metadata
- Download URL: pycityvisitorparking-0.5.23.tar.gz
- Upload date:
- Size: 69.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be11d2d82d41fb60f8c3e66fb3a40a8ca80120692462a598f5dbe75c7c88da79
|
|
| MD5 |
78db731259bdfa19719e9ea65af4700d
|
|
| BLAKE2b-256 |
b430e9e78f7b76cba16f97730fd56d480880025a89d373af1a6fae6d7b784608
|
Provenance
The following attestation bundles were made for pycityvisitorparking-0.5.23.tar.gz:
Publisher:
release.yml on sir-Unknown/pyCityVisitorParking
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycityvisitorparking-0.5.23.tar.gz -
Subject digest:
be11d2d82d41fb60f8c3e66fb3a40a8ca80120692462a598f5dbe75c7c88da79 - Sigstore transparency entry: 1289431702
- Sigstore integration time:
-
Permalink:
sir-Unknown/pyCityVisitorParking@ac7e22a25ec73ae1f848eb0a8db4327812382ddf -
Branch / Tag:
refs/tags/v0.5.23 - Owner: https://github.com/sir-Unknown
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ac7e22a25ec73ae1f848eb0a8db4327812382ddf -
Trigger Event:
release
-
Statement type:
File details
Details for the file pycityvisitorparking-0.5.23-py3-none-any.whl.
File metadata
- Download URL: pycityvisitorparking-0.5.23-py3-none-any.whl
- Upload date:
- Size: 66.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
40703578b40eb7bd1cc242fb373e314a7311ca4035424c5b732026e28ffa17e4
|
|
| MD5 |
edc174dd144bafc1496674e53833f3d6
|
|
| BLAKE2b-256 |
ed729b6db93d4cb63ea4ddb861c3860ad6e2f75bde0c346e9b88c238b138b545
|
Provenance
The following attestation bundles were made for pycityvisitorparking-0.5.23-py3-none-any.whl:
Publisher:
release.yml on sir-Unknown/pyCityVisitorParking
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pycityvisitorparking-0.5.23-py3-none-any.whl -
Subject digest:
40703578b40eb7bd1cc242fb373e314a7311ca4035424c5b732026e28ffa17e4 - Sigstore transparency entry: 1289431783
- Sigstore integration time:
-
Permalink:
sir-Unknown/pyCityVisitorParking@ac7e22a25ec73ae1f848eb0a8db4327812382ddf -
Branch / Tag:
refs/tags/v0.5.23 - Owner: https://github.com/sir-Unknown
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@ac7e22a25ec73ae1f848eb0a8db4327812382ddf -
Trigger Event:
release
-
Statement type: