Async integration layer between Home Assistant and Dutch municipal visitor parking APIs.
Project description
pyCityVisitorParking
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:
- 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
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_urlis required for provider requests.api_uriis optional; providers may define a default (see provider README).- Credentials are standardized:
usernameandpasswordare 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()returnsProviderInfowithid,favorite_update_fields, andreservation_update_fields. - Permit:
get_permit()returnsPermitwithid,remaining_balance(minutes), andzone_validity. - Zone validity: each
ZoneValidityBlockincludesstart_timeandend_time(UTC ISO 8601). - Reservations:
list_reservations(),start_reservation(),update_reservation(), andend_reservation()returnReservationwithid,name,license_plate,start_time, andend_time. Some providers only support updatingend_time(seereservation_update_fields). - Favorites:
list_favorites()andadd_favorite()returnFavoritewithid,name, andlicense_plate.update_favorite()returnsFavoritewhen supported (favorite_update_fieldsis non-empty), otherwise it raisesProviderError.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, andConfigErrorfor 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
datetimevalues; naive timestamps raiseValidationError. - All public timestamps are normalized to UTC ISO 8601 with
Zand second precision (microseconds are truncated). - Internally, reservation times are handled as timezone-aware UTC
datetimevalues and serialized to strings only at provider and model boundaries. - License plates are normalized to uppercase
A-Z0-9without spaces/symbols. - Adding a favorite that already exists by license plate raises
ValidationError. zone_validitymust 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
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.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25a94f7edf634fdf694323c3a67bbed32dfdad3584c7fa218b6449a5a3d4b046
|
|
| MD5 |
744533e37edf26f14633327ea8378ea0
|
|
| BLAKE2b-256 |
bd91f4837b1d38de17c791fe0fa5648bdd6c3eb9f8c90839492f9387e208be4d
|
Provenance
The following attestation bundles were made for pycityvisitorparking-0.5.10.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.10.tar.gz -
Subject digest:
25a94f7edf634fdf694323c3a67bbed32dfdad3584c7fa218b6449a5a3d4b046 - Sigstore transparency entry: 805840878
- Sigstore integration time:
-
Permalink:
sir-Unknown/pyCityVisitorParking@b559272b4db76233aa64c8df992c49bc273deccd -
Branch / Tag:
refs/heads/main - Owner: https://github.com/sir-Unknown
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b559272b4db76233aa64c8df992c49bc273deccd -
Trigger Event:
workflow_run
-
Statement type:
File details
Details for the file pycityvisitorparking-0.5.10-py3-none-any.whl.
File metadata
- Download URL: pycityvisitorparking-0.5.10-py3-none-any.whl
- Upload date:
- Size: 46.9 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 |
60bc361bed970f8f57afd0cab3935f566ed7e8890c51c04331dd9a3d12b59cb2
|
|
| MD5 |
01aaa91d02ffaada801330c1fa2a3852
|
|
| BLAKE2b-256 |
91067ce373a419896d3593a56f173ae0c75b100281c0ffdf3d308e5832e3d803
|
Provenance
The following attestation bundles were made for pycityvisitorparking-0.5.10-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.10-py3-none-any.whl -
Subject digest:
60bc361bed970f8f57afd0cab3935f566ed7e8890c51c04331dd9a3d12b59cb2 - Sigstore transparency entry: 805840948
- Sigstore integration time:
-
Permalink:
sir-Unknown/pyCityVisitorParking@b559272b4db76233aa64c8df992c49bc273deccd -
Branch / Tag:
refs/heads/main - Owner: https://github.com/sir-Unknown
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b559272b4db76233aa64c8df992c49bc273deccd -
Trigger Event:
workflow_run
-
Statement type: