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.
- pycityvisitorparking/__init__.py +28 -0
- pycityvisitorparking/_version.py +3 -0
- pycityvisitorparking/client.py +83 -0
- pycityvisitorparking/exceptions.py +21 -0
- pycityvisitorparking/models.py +40 -0
- pycityvisitorparking/provider/AGENTS.md +130 -0
- pycityvisitorparking/provider/README.md +173 -0
- pycityvisitorparking/provider/__init__.py +1 -0
- pycityvisitorparking/provider/base.py +280 -0
- pycityvisitorparking/provider/dvsportal/CHANGELOG.md +5 -0
- pycityvisitorparking/provider/dvsportal/README.md +119 -0
- pycityvisitorparking/provider/dvsportal/__init__.py +5 -0
- pycityvisitorparking/provider/dvsportal/api.py +637 -0
- pycityvisitorparking/provider/dvsportal/const.py +19 -0
- pycityvisitorparking/provider/dvsportal/manifest.json +5 -0
- pycityvisitorparking/provider/dvsportal/tests/test_mapping.py +186 -0
- pycityvisitorparking/provider/loader.py +90 -0
- pycityvisitorparking/provider/manifest.schema.json +19 -0
- pycityvisitorparking/provider/the_hague/CHANGELOG.md +5 -0
- pycityvisitorparking/provider/the_hague/README.md +75 -0
- pycityvisitorparking/provider/the_hague/__init__.py +5 -0
- pycityvisitorparking/provider/the_hague/api.py +483 -0
- pycityvisitorparking/provider/the_hague/const.py +17 -0
- pycityvisitorparking/provider/the_hague/manifest.json +5 -0
- pycityvisitorparking/provider/the_hague/tests/test_mapping.py +105 -0
- pycityvisitorparking/util.py +103 -0
- pycityvisitorparking-0.2.0.dist-info/METADATA +115 -0
- pycityvisitorparking-0.2.0.dist-info/RECORD +30 -0
- pycityvisitorparking-0.2.0.dist-info/WHEEL +4 -0
- 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,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."""
|