pycityvisitorparking 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. pycityvisitorparking/__init__.py +28 -0
  2. pycityvisitorparking/_version.py +3 -0
  3. pycityvisitorparking/client.py +83 -0
  4. pycityvisitorparking/exceptions.py +21 -0
  5. pycityvisitorparking/models.py +40 -0
  6. pycityvisitorparking/provider/AGENTS.md +130 -0
  7. pycityvisitorparking/provider/README.md +173 -0
  8. pycityvisitorparking/provider/__init__.py +1 -0
  9. pycityvisitorparking/provider/base.py +280 -0
  10. pycityvisitorparking/provider/dvsportal/CHANGELOG.md +5 -0
  11. pycityvisitorparking/provider/dvsportal/README.md +119 -0
  12. pycityvisitorparking/provider/dvsportal/__init__.py +5 -0
  13. pycityvisitorparking/provider/dvsportal/api.py +637 -0
  14. pycityvisitorparking/provider/dvsportal/const.py +19 -0
  15. pycityvisitorparking/provider/dvsportal/manifest.json +5 -0
  16. pycityvisitorparking/provider/dvsportal/tests/test_mapping.py +186 -0
  17. pycityvisitorparking/provider/loader.py +90 -0
  18. pycityvisitorparking/provider/manifest.schema.json +19 -0
  19. pycityvisitorparking/provider/the_hague/CHANGELOG.md +5 -0
  20. pycityvisitorparking/provider/the_hague/README.md +75 -0
  21. pycityvisitorparking/provider/the_hague/__init__.py +5 -0
  22. pycityvisitorparking/provider/the_hague/api.py +483 -0
  23. pycityvisitorparking/provider/the_hague/const.py +17 -0
  24. pycityvisitorparking/provider/the_hague/manifest.json +5 -0
  25. pycityvisitorparking/provider/the_hague/tests/test_mapping.py +105 -0
  26. pycityvisitorparking/util.py +103 -0
  27. pycityvisitorparking-0.2.0.dist-info/METADATA +115 -0
  28. pycityvisitorparking-0.2.0.dist-info/RECORD +30 -0
  29. pycityvisitorparking-0.2.0.dist-info/WHEEL +4 -0
  30. pycityvisitorparking-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,28 @@
1
+ """pyCityVisitorParking package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ from .client import Client
8
+ from .exceptions import AuthError, NetworkError, ProviderError, ValidationError
9
+ from .models import Favorite, Permit, ProviderInfo, Reservation, ZoneValidityBlock
10
+
11
+ try:
12
+ __version__ = version("pycityvisitorparking")
13
+ except PackageNotFoundError: # pragma: no cover - not installed
14
+ __version__ = "0.0.0"
15
+
16
+ __all__ = [
17
+ "AuthError",
18
+ "Client",
19
+ "Favorite",
20
+ "NetworkError",
21
+ "Permit",
22
+ "ProviderError",
23
+ "ProviderInfo",
24
+ "Reservation",
25
+ "ValidationError",
26
+ "ZoneValidityBlock",
27
+ "__version__",
28
+ ]
@@ -0,0 +1,3 @@
1
+ """Package version for Hatch."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,83 @@
1
+ """Client facade for provider discovery and instantiation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+
7
+ import aiohttp
8
+
9
+ from .exceptions import ProviderError
10
+ from .models import ProviderInfo
11
+ from .provider.base import BaseProvider
12
+ from .provider.loader import get_manifest, list_providers
13
+
14
+ _DEFAULT_TIMEOUT = aiohttp.ClientTimeout(total=30)
15
+
16
+
17
+ class Client:
18
+ """Facade for provider discovery and access."""
19
+
20
+ def __init__(
21
+ self,
22
+ session: aiohttp.ClientSession | None = None,
23
+ *,
24
+ base_url: str | None = None,
25
+ api_uri: str | None = None,
26
+ timeout: aiohttp.ClientTimeout | None = None,
27
+ retry_count: int = 0,
28
+ ) -> None:
29
+ self._session = session
30
+ self._owns_session = session is None
31
+ self._base_url = base_url
32
+ self._api_uri = api_uri
33
+ self._timeout = timeout or _DEFAULT_TIMEOUT
34
+ self._retry_count = max(0, retry_count)
35
+
36
+ async def __aenter__(self) -> Client:
37
+ return self
38
+
39
+ async def __aexit__(self, exc_type, exc, tb) -> None:
40
+ await self.aclose()
41
+
42
+ async def aclose(self) -> None:
43
+ if self._owns_session and self._session is not None:
44
+ await self._session.close()
45
+ self._session = None
46
+
47
+ async def list_providers(self) -> list[ProviderInfo]:
48
+ return list_providers()
49
+
50
+ async def get_provider(
51
+ self,
52
+ provider_id: str,
53
+ *,
54
+ base_url: str | None = None,
55
+ api_uri: str | None = None,
56
+ ) -> BaseProvider:
57
+ if not provider_id:
58
+ raise ProviderError("Provider id is required.")
59
+ manifest = get_manifest(provider_id)
60
+ module_name = f"pycityvisitorparking.provider.{provider_id}"
61
+ try:
62
+ module = importlib.import_module(module_name)
63
+ except ModuleNotFoundError as exc:
64
+ raise ProviderError("Provider module could not be imported.") from exc
65
+ provider_cls = getattr(module, "Provider", None)
66
+ if provider_cls is None:
67
+ raise ProviderError("Provider module does not export Provider.")
68
+ if not isinstance(provider_cls, type) or not issubclass(provider_cls, BaseProvider):
69
+ raise ProviderError("Provider must inherit from BaseProvider.")
70
+ session = self._ensure_session()
71
+ return provider_cls(
72
+ session,
73
+ manifest,
74
+ base_url=base_url if base_url is not None else self._base_url,
75
+ api_uri=api_uri if api_uri is not None else self._api_uri,
76
+ timeout=self._timeout,
77
+ retry_count=self._retry_count,
78
+ )
79
+
80
+ def _ensure_session(self) -> aiohttp.ClientSession:
81
+ if self._session is None:
82
+ self._session = aiohttp.ClientSession(timeout=self._timeout)
83
+ return self._session
@@ -0,0 +1,21 @@
1
+ """Library exceptions."""
2
+
3
+
4
+ class PyCityVisitorParkingError(Exception):
5
+ """Base exception for the library."""
6
+
7
+
8
+ class AuthError(PyCityVisitorParkingError):
9
+ """Raised when authentication fails."""
10
+
11
+
12
+ class NetworkError(PyCityVisitorParkingError):
13
+ """Raised when network communication fails."""
14
+
15
+
16
+ class ValidationError(PyCityVisitorParkingError):
17
+ """Raised when inputs fail validation."""
18
+
19
+
20
+ class ProviderError(PyCityVisitorParkingError):
21
+ """Raised when a provider returns an error or is misconfigured."""
@@ -0,0 +1,40 @@
1
+ """Public data models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class ProviderInfo:
10
+ id: str
11
+ favorite_update_possible: bool
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class ZoneValidityBlock:
16
+ start_time: str
17
+ end_time: str
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class Permit:
22
+ id: str
23
+ remaining_time: int
24
+ zone_validity: list[ZoneValidityBlock]
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class Reservation:
29
+ id: str
30
+ name: str
31
+ license_plate: str
32
+ start_time: str
33
+ end_time: str
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class Favorite:
38
+ id: str
39
+ name: str
40
+ license_plate: str
@@ -0,0 +1,130 @@
1
+ # AGENTS.md — Provider Authoring Guide (pyCityVisitorParking)
2
+
3
+ ✅ **Limit the PR to the provider folder only**
4
+ - Add a provider by adding files only under:
5
+ `src/pycityvisitorparking/provider/<provider_id>/...`
6
+ - ❌ Avoid modifying core code, models, loader, or shared tests for a provider PR.
7
+
8
+ ✅ **Use English as the primary project language**
9
+ - Write all documentation, comments, docstrings, commit messages, and changelogs in English.
10
+ - Use Dutch only when quoting provider UI text or official municipality wording, and label it clearly.
11
+
12
+ ✅ **Create the required provider structure**
13
+ - Create `src/pycityvisitorparking/provider/<provider_id>/`.
14
+ - Add these required files:
15
+ - `manifest.json`
16
+ - `__init__.py`
17
+ - `api.py`
18
+ - `const.py`
19
+ - `README.md`
20
+ - `CHANGELOG.md`
21
+ - Add provider-specific tests and fixtures inside the provider folder when possible.
22
+
23
+ ✅ **Maintain provider documentation and changelog**
24
+ - Write `README.md` inside the provider folder and include:
25
+ - provider description and official service name
26
+ - required login keys and example credential shapes
27
+ - supported operations and known limitations
28
+ - mapping notes for `zone_validity` and time handling
29
+ - links to official provider documentation where available
30
+ - Write `CHANGELOG.md` inside the provider folder.
31
+ - Keep an “Unreleased” section in the provider changelog.
32
+ - Update provider `CHANGELOG.md` whenever endpoints, auth flow, mapping behavior, or limitations change.
33
+
34
+ ✅ **Verify documentation and pinned versions and correct assumptions**
35
+ - Check the current project documentation and the versions pinned in our lockfiles and CI.
36
+ - Update any assumptions immediately when documentation, lockfiles, and CI disagree.
37
+ - Align any provider docs, examples, and test expectations with the pinned versions.
38
+ - Avoid “latest” installs in CI and keep dev tooling versions pinned.
39
+
40
+ ✅ **Require docstrings and clear comments**
41
+ - Add docstrings for all public modules, classes, and methods.
42
+ - Add clear inline comments for non-obvious logic (e.g., time/UTC conversion, normalization, fallback behavior).
43
+ - Keep comments/docstrings free of credentials, tokens, or other PII.
44
+
45
+ ✅ **Create a valid provider manifest**
46
+ - Create `manifest.json` and include:
47
+ - `id` and match it exactly to the folder name
48
+ - `name`
49
+ - `favorite_update_possible`
50
+ - Validate `manifest.json` against:
51
+ `src/pycityvisitorparking/provider/manifest.schema.json`
52
+ - ❌ Avoid imports or side effects when defining the manifest.
53
+ * Use integration-provided `base_url` and `api_uri` to build request URLs.
54
+ * ❌ Do not embed municipality base URLs in provider code or manifests.
55
+
56
+ ✅ **Export the Provider class without side effects**
57
+ - Export a class named `Provider` from `<provider_id>/__init__.py`.
58
+ - Implement `pycityvisitorparking.provider.base.BaseProvider`.
59
+ - ❌ Avoid network calls, file writes, or heavy work at import time.
60
+
61
+ ✅ **Map strictly to public dataclasses**
62
+ - Map provider responses only into strict public dataclasses:
63
+ - `ProviderInfo`
64
+ - `Permit`
65
+ - `ZoneValidityBlock`
66
+ - `Reservation`
67
+ - `Favorite`
68
+ - ❌ Avoid adding provider-specific fields to public models.
69
+
70
+ ✅ **Normalize license plates (mandatory)**
71
+ - Normalize every plate to uppercase `A–Z0–9`.
72
+ - Remove spaces and special characters.
73
+ - Raise `ValidationError` for invalid or empty plates after normalization.
74
+
75
+ ✅ **Convert all timestamps to UTC**
76
+ - Convert all provider timestamps to UTC before returning them.
77
+ - Format all public timestamps as ISO 8601 with `Z` and without microseconds.
78
+ - Ensure every returned timestamp is parseable as a timezone-aware UTC datetime.
79
+
80
+ ✅ **Filter zone_validity to chargeable windows**
81
+ - Build `Permit.zone_validity` using only chargeable windows.
82
+ - Filter out free windows.
83
+ - Include windows only when reservation is required/chargeable.
84
+
85
+ ✅ **Implement login and auth safely**
86
+ - Accept credentials via `Mapping[str, str]` or `**kwargs`.
87
+ - Validate required keys and raise `ValidationError` when missing.
88
+ - Raise `AuthError` for invalid credentials or expired sessions.
89
+ - ❌ Avoid logging credentials or tokens.
90
+
91
+ ✅ **Use aiohttp and map errors safely**
92
+ - Use the injected `aiohttp.ClientSession`.
93
+ - Enforce timeouts and keep TLS verification enabled.
94
+ - Translate errors into library exceptions.
95
+ - ❌ Avoid leaking raw `aiohttp` exceptions.
96
+ - Use relative paths and build requests from `base_url + api_uri`.
97
+
98
+ ✅ **Respect favorite update behavior**
99
+ - Set `favorite_update_possible` to `true` only when native update is supported.
100
+ - Set `favorite_update_possible` to `false` when native update is not supported.
101
+ - Rely on core fallback behavior (remove+add) when update is not supported.
102
+
103
+ ✅ **Enforce reservation rules**
104
+ - Require `end_time` for `start_reservation` and ❌ avoid defaults.
105
+ - Enforce `end_time > start_time` and raise `ValidationError` when violated.
106
+
107
+ ✅ **Add tests without live calls**
108
+ - ❌ Avoid live services.
109
+ - Use fixtures and mocks.
110
+ - Add tests that verify:
111
+ - license plate normalization
112
+ - UTC conversion from an offset timestamp
113
+ - `zone_validity` filtering to chargeable windows
114
+ - mapping into strict public dataclasses
115
+
116
+ ✅ **Make provider changes release-ready**
117
+ - Update provider `CHANGELOG.md` when provider behavior changes.
118
+ - Keep provider `README.md` accurate for required login keys and limitations.
119
+ - ❌ Avoid adding new runtime dependencies without maintainer approval.
120
+
121
+ ✅ **Run the checklist before opening a PR**
122
+ - Confirm adding files only under `provider/<provider_id>/`.
123
+ - Confirm `manifest.json` validates against the schema.
124
+ - Confirm `Provider` exports and ❌ avoid network calls on import.
125
+ - Confirm UTC ISO 8601 `Z` timestamps in all public outputs.
126
+ - Confirm license plate normalization and validation.
127
+ - Confirm chargeable-only `zone_validity`.
128
+ - Confirm `end_time` required and `end_time > start_time`.
129
+ - Confirm provider `README.md` and `CHANGELOG.md` exist and are updated.
130
+ - Confirm `pytest` passes.
@@ -0,0 +1,173 @@
1
+ # Provider Authoring Guide
2
+
3
+ This document explains how to add a new provider to `pyCityVisitorParking`.
4
+ Provider PRs must touch only `src/pycityvisitorparking/provider/<provider_id>/...`.
5
+
6
+ ## Required folder structure
7
+
8
+ Create a new folder under `src/pycityvisitorparking/provider/`:
9
+
10
+ ```
11
+ src/pycityvisitorparking/provider/<provider_id>/
12
+ manifest.json
13
+ __init__.py
14
+ api.py
15
+ const.py
16
+ README.md
17
+ CHANGELOG.md
18
+ ```
19
+
20
+ The `<provider_id>` must be lowercase and match the `id` field in `manifest.json`.
21
+
22
+ ## Manifest requirements
23
+
24
+ Each provider must include a `manifest.json` with:
25
+
26
+ ```json
27
+ {
28
+ "id": "<provider_id>",
29
+ "name": "Display Name",
30
+ "favorite_update_possible": true
31
+ }
32
+ ```
33
+
34
+ Manifests are validated against:
35
+ `src/pycityvisitorparking/provider/manifest.schema.json`.
36
+
37
+ ## Provider class export
38
+
39
+ The provider package must export a class named `Provider` from
40
+ `src/pycityvisitorparking/provider/<provider_id>/__init__.py`:
41
+
42
+ ```python
43
+ from .api import Provider
44
+
45
+ __all__ = ["Provider"]
46
+ ```
47
+
48
+ The `Provider` class must inherit `pycityvisitorparking.provider.base.BaseProvider`
49
+ and implement all required async methods.
50
+
51
+ ## Import safety
52
+
53
+ Discovery reads manifests without importing provider code. Avoid side effects on
54
+ import:
55
+
56
+ - No network requests
57
+ - No file writes
58
+ - No heavy computation
59
+
60
+ All setup should happen inside async methods.
61
+
62
+ ## Base URL and API URI
63
+
64
+ Home Assistant supplies `base_url` and optionally `api_uri`. Provider code must
65
+ use relative paths and join them with `base_url + api_uri` via
66
+ `BaseProvider._build_url()` or `_request_json()` / `_request_text()`.
67
+
68
+ Do not hardcode `base_url` or `api_uri` in provider code.
69
+
70
+ ## Authentication
71
+
72
+ `login()` must accept `Mapping[str, str]` or `**kwargs`. Validate required keys
73
+ and raise `ValidationError` for missing values. Raise `AuthError` for invalid
74
+ credentials or expired sessions.
75
+
76
+ Do not log credentials or tokens.
77
+
78
+ ## Time handling (UTC required)
79
+
80
+ All public timestamps must be UTC ISO 8601 with a trailing `Z` and no
81
+ microseconds. Convert provider-local or offset timestamps to UTC internally.
82
+
83
+ Use utilities from `pycityvisitorparking.util`:
84
+
85
+ - `ensure_utc_timestamp()`
86
+ - `format_utc_timestamp()`
87
+ - `parse_timestamp()`
88
+
89
+ All timestamps must be timezone-aware and parseable.
90
+
91
+ ## License plate normalization
92
+
93
+ Normalize plates to uppercase `A-Z0-9` and remove spaces/special characters.
94
+ Raise `ValidationError` for invalid or empty plates after normalization.
95
+
96
+ Use `normalize_license_plate()` from `pycityvisitorparking.util`.
97
+
98
+ ## Zone validity filtering
99
+
100
+ `Permit.zone_validity` must include only chargeable windows.
101
+ Filter out free or non-chargeable windows. Use
102
+ `filter_chargeable_zone_validity()` from `pycityvisitorparking.util`.
103
+
104
+ ## Reservation validation
105
+
106
+ `start_reservation()` requires both `start_time` and `end_time`.
107
+ Enforce `end_time > start_time` and raise `ValidationError` when violated.
108
+
109
+ Use `validate_reservation_times()` for shared validation and normalization.
110
+
111
+ ## Favorites update behavior
112
+
113
+ Set `favorite_update_possible` in the manifest:
114
+
115
+ - `true` only if the provider supports a native update endpoint.
116
+ - `false` if updates require delete + add.
117
+
118
+ When `favorite_update_possible` is `false`, the core fallback removes and re-adds
119
+ the favorite. Providers should implement `_update_favorite_native()` even if the
120
+ fallback is used (it can raise `ProviderError` if called unexpectedly).
121
+
122
+ ## Public models only
123
+
124
+ Map all provider responses to strict public dataclasses in
125
+ `pycityvisitorparking.models`:
126
+
127
+ - `ProviderInfo`
128
+ - `Permit`
129
+ - `ZoneValidityBlock`
130
+ - `Reservation`
131
+ - `Favorite`
132
+
133
+ Do not add provider-specific fields to public models.
134
+
135
+ ## HTTP safety
136
+
137
+ Use the injected `aiohttp.ClientSession` from `BaseProvider`.
138
+ Enable TLS verification and enforce timeouts.
139
+ Translate network and HTTP errors into library exceptions.
140
+ Never leak raw `aiohttp` exceptions.
141
+
142
+ Retries are allowed only for idempotent GET requests and should be conservative
143
+ or disabled.
144
+
145
+ ## Logging and PII
146
+
147
+ Do not log credentials, tokens, or full license plates. If logging is necessary,
148
+ mask plates using `mask_license_plate()`.
149
+
150
+ ## Documentation and changelog
151
+
152
+ Each provider must include:
153
+
154
+ - `README.md` with auth requirements, supported operations, limitations,
155
+ mapping notes, and links to official docs.
156
+ - `CHANGELOG.md` with an `Unreleased` section. Update it when auth, endpoints,
157
+ mappings, or limitations change.
158
+
159
+ ## Tests
160
+
161
+ Provider tests should use mocks/fixtures and avoid live services.
162
+ Add coverage for:
163
+
164
+ - license plate normalization
165
+ - UTC conversion from provider timestamps
166
+ - zone validity filtering
167
+ - mapping into public models
168
+
169
+ Run the test suite with:
170
+
171
+ ```bash
172
+ hatch run test:run
173
+ ```
@@ -0,0 +1 @@
1
+ """Provider package."""