permisapi-client 0.6.0__tar.gz → 0.7.1__tar.gz
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.
- {permisapi_client-0.6.0 → permisapi_client-0.7.1}/.gitignore +3 -0
- {permisapi_client-0.6.0 → permisapi_client-0.7.1}/PKG-INFO +1 -1
- {permisapi_client-0.6.0 → permisapi_client-0.7.1}/permisapi_client/__init__.py +1 -1
- {permisapi_client-0.6.0 → permisapi_client-0.7.1}/permisapi_client/client.py +51 -2
- {permisapi_client-0.6.0 → permisapi_client-0.7.1}/pyproject.toml +1 -1
- {permisapi_client-0.6.0 → permisapi_client-0.7.1}/tests/test_client.py +68 -0
- {permisapi_client-0.6.0 → permisapi_client-0.7.1}/README.md +0 -0
- {permisapi_client-0.6.0 → permisapi_client-0.7.1}/permisapi_client/exceptions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: permisapi-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Client Python officiel pour PermisAPI : permis de construire France en API REST.
|
|
5
5
|
Project-URL: Homepage, https://permisapi.fr
|
|
6
6
|
Project-URL: Documentation, https://permisapi.fr/docs
|
|
@@ -13,7 +13,7 @@ from permisapi_client.exceptions import error_from_response
|
|
|
13
13
|
|
|
14
14
|
DEFAULT_BASE_URL = "https://api.permisapi.fr"
|
|
15
15
|
DEFAULT_TIMEOUT = 15.0
|
|
16
|
-
USER_AGENT = "permisapi-client-python/0.
|
|
16
|
+
USER_AGENT = "permisapi-client-python/0.7.1"
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _raise_for_status(resp: httpx.Response) -> None:
|
|
@@ -52,7 +52,18 @@ class _PermitsResource:
|
|
|
52
52
|
self._c = client
|
|
53
53
|
|
|
54
54
|
def list(self, **params: Any) -> dict[str, Any]:
|
|
55
|
-
"""GET /v1/permits avec filtres. Retourne {
|
|
55
|
+
"""GET /v1/permits avec filtres. Retourne {data, pagination}.
|
|
56
|
+
|
|
57
|
+
Filtres acceptes : dep_code, comm_code, localite, codpost,
|
|
58
|
+
permit_type, etat_pa, an_depot, date_from, date_to,
|
|
59
|
+
min_superficie, siren, geocoded_only, page, limit.
|
|
60
|
+
|
|
61
|
+
Tri : passer `sort="-date_reelle_autorisation"` (defaut),
|
|
62
|
+
`sort="date_depot"`, `sort="-superficie_terrain"`, etc.
|
|
63
|
+
Champs autorises : date_reelle_autorisation, date_depot,
|
|
64
|
+
an_depot, superficie_terrain. Prefixe `-` pour descendant.
|
|
65
|
+
Tout autre champ retourne 422 (et non ignore silencieusement).
|
|
66
|
+
"""
|
|
56
67
|
return self._c._get("/v1/permits", params=params)
|
|
57
68
|
|
|
58
69
|
def iter_all(self, **params: Any) -> Iterator[dict[str, Any]]:
|
|
@@ -177,6 +188,33 @@ class _PermitsResource:
|
|
|
177
188
|
"""
|
|
178
189
|
return self._c._get(f"/v1/permits/{num_pa}/plu")
|
|
179
190
|
|
|
191
|
+
def full_view(self, num_pa: str) -> dict[str, Any]:
|
|
192
|
+
"""GET /v1/permits/{num_pa}/360 : vue agregee 6-en-1 en 1 appel HTTP.
|
|
193
|
+
|
|
194
|
+
Combine en 1 reponse : detail + sirene + dvf + score + plu + risks.
|
|
195
|
+
Equivalent fonctionnel de 6 calls separes mais en 1 round-trip,
|
|
196
|
+
avec parallelisation cote serveur des sub-fetches lents (PLU
|
|
197
|
+
apicarto + Risks BRGM).
|
|
198
|
+
|
|
199
|
+
Cout quota : 6 unites du quota mensuel pour Pro+, 1 unite pour
|
|
200
|
+
Free / Explorer (qui recoivent juste `detail`, autres champs `null`
|
|
201
|
+
avec `plan_required_for_full="pro"`). Le header de reponse
|
|
202
|
+
`X-RateLimit-Cost` confirme le cout decompte.
|
|
203
|
+
|
|
204
|
+
Best-effort : si une sous-feature echoue (timeout, 5xx tiers),
|
|
205
|
+
son champ vaut `null` et l'erreur est listee dans `fetch_errors`.
|
|
206
|
+
Les autres sous-features ne sont pas affectees.
|
|
207
|
+
|
|
208
|
+
Latence typique 5-7s (dominee par PLU + Risks live, paralleles).
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
dict avec cles : num_pa, detail, sirene, dvf, score, plu,
|
|
212
|
+
risks, fetch_errors (list), subfetches_billed (int).
|
|
213
|
+
Si plan Free/Explorer : `plan_required_for_full="pro"` est
|
|
214
|
+
present et toutes les sous-features sauf detail sont null.
|
|
215
|
+
"""
|
|
216
|
+
return self._c._get(f"/v1/permits/{num_pa}/360")
|
|
217
|
+
|
|
180
218
|
def score_feedback(
|
|
181
219
|
self,
|
|
182
220
|
*,
|
|
@@ -435,6 +473,8 @@ class _AsyncPermitsResource:
|
|
|
435
473
|
self._c = client
|
|
436
474
|
|
|
437
475
|
async def list(self, **params: Any) -> dict[str, Any]:
|
|
476
|
+
"""GET /v1/permits avec filtres. Voir _PermitsResource.list pour
|
|
477
|
+
la doc complete (filtres + tri sort=)."""
|
|
438
478
|
return await self._c._get("/v1/permits", params=params)
|
|
439
479
|
|
|
440
480
|
async def iter_all(self, **params: Any) -> AsyncIterator[dict[str, Any]]:
|
|
@@ -506,6 +546,15 @@ class _AsyncPermitsResource:
|
|
|
506
546
|
Voir _PermitsResource.risks pour la doc complete."""
|
|
507
547
|
return await self._c._get(f"/v1/permits/{num_pa}/risks")
|
|
508
548
|
|
|
549
|
+
async def full_view(self, num_pa: str) -> dict[str, Any]:
|
|
550
|
+
"""GET /v1/permits/{num_pa}/360 : version async. Vue agregee 6-en-1.
|
|
551
|
+
|
|
552
|
+
Cout quota : 6 unites pour Pro+, 1 unite pour Free / Explorer.
|
|
553
|
+
Le header `X-RateLimit-Cost` confirme le cout. Voir
|
|
554
|
+
_PermitsResource.full_view pour la doc complete.
|
|
555
|
+
"""
|
|
556
|
+
return await self._c._get(f"/v1/permits/{num_pa}/360")
|
|
557
|
+
|
|
509
558
|
async def score_feedback(
|
|
510
559
|
self,
|
|
511
560
|
*,
|
|
@@ -337,6 +337,74 @@ async def test_async_permits_export() -> None:
|
|
|
337
337
|
assert b"PC-A,75" in result
|
|
338
338
|
|
|
339
339
|
|
|
340
|
+
@respx.mock
|
|
341
|
+
def test_permits_list_with_sort() -> None:
|
|
342
|
+
"""SDK propagates `sort=` query param transparently to the API."""
|
|
343
|
+
route = respx.get(f"{BASE}/v1/permits").respond(
|
|
344
|
+
json={
|
|
345
|
+
"data": [{"num_pa": "PC-1", "superficie_terrain": 1500}],
|
|
346
|
+
"pagination": {"page": 1, "total": 1, "pages": 1},
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
with PermisAPI(api_key="pk_test_abc") as client:
|
|
350
|
+
client.permits.list(dep_code="33", sort="-superficie_terrain", limit=10)
|
|
351
|
+
assert route.called
|
|
352
|
+
# The sort param must appear in the request URL query string
|
|
353
|
+
url = str(route.calls.last.request.url)
|
|
354
|
+
assert "sort=-superficie_terrain" in url
|
|
355
|
+
assert "dep_code=33" in url
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@respx.mock
|
|
359
|
+
def test_permits_full_view_sync() -> None:
|
|
360
|
+
"""Sync : full_view appelle GET /v1/permits/{num_pa}/360 et retourne
|
|
361
|
+
le payload agrege complet."""
|
|
362
|
+
respx.get(f"{BASE}/v1/permits/PC-360-001/360").respond(
|
|
363
|
+
json={
|
|
364
|
+
"num_pa": "PC-360-001",
|
|
365
|
+
"detail": {"num_pa": "PC-360-001", "dep_code": "75"},
|
|
366
|
+
"sirene": {"match": {"siren": "111222333"}},
|
|
367
|
+
"dvf": {"matches": []},
|
|
368
|
+
"score": {"score": 72, "tier": "high"},
|
|
369
|
+
"plu": {"zonage": {"code": "UA", "constructible": True}},
|
|
370
|
+
"risks": {"risk_tier": "low"},
|
|
371
|
+
"fetch_errors": [],
|
|
372
|
+
"subfetches_billed": 6,
|
|
373
|
+
}
|
|
374
|
+
)
|
|
375
|
+
with PermisAPI(api_key="pk_test_abc") as client:
|
|
376
|
+
view = client.permits.full_view("PC-360-001")
|
|
377
|
+
assert view["num_pa"] == "PC-360-001"
|
|
378
|
+
assert view["subfetches_billed"] == 6
|
|
379
|
+
assert view["score"]["tier"] == "high"
|
|
380
|
+
assert view["plu"]["zonage"]["constructible"] is True
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@pytest.mark.asyncio
|
|
384
|
+
@respx.mock
|
|
385
|
+
async def test_async_permits_full_view() -> None:
|
|
386
|
+
"""Async : full_view appelle GET /v1/permits/{num_pa}/360."""
|
|
387
|
+
respx.get(f"{BASE}/v1/permits/PC-FREE/360").respond(
|
|
388
|
+
json={
|
|
389
|
+
"num_pa": "PC-FREE",
|
|
390
|
+
"detail": {"num_pa": "PC-FREE"},
|
|
391
|
+
"sirene": None,
|
|
392
|
+
"dvf": None,
|
|
393
|
+
"score": None,
|
|
394
|
+
"plu": None,
|
|
395
|
+
"risks": None,
|
|
396
|
+
"fetch_errors": [],
|
|
397
|
+
"plan_required_for_full": "pro",
|
|
398
|
+
"subfetches_billed": 1,
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
async with AsyncPermisAPI(api_key="pk_test_abc") as client:
|
|
402
|
+
view = await client.permits.full_view("PC-FREE")
|
|
403
|
+
assert view["plan_required_for_full"] == "pro"
|
|
404
|
+
assert view["subfetches_billed"] == 1
|
|
405
|
+
assert view["sirene"] is None
|
|
406
|
+
|
|
407
|
+
|
|
340
408
|
def test_custom_base_url() -> None:
|
|
341
409
|
client = PermisAPI(api_key="pk_test_abc", base_url="http://localhost:8000/")
|
|
342
410
|
assert client.base_url == "http://localhost:8000"
|
|
File without changes
|
|
File without changes
|