permisapi-client 0.6.0__tar.gz → 0.7.0__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.
@@ -63,3 +63,6 @@ landing/out/
63
63
  # Post-piratage rotation (contient les nouvelles cles API en clair)
64
64
  secrets_NEW_KEYS.json
65
65
  scrape-downloads/
66
+
67
+ # Local handoff bundle (assembled then zipped, not committed)
68
+ handoff-bundle/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: permisapi-client
3
- Version: 0.6.0
3
+ Version: 0.7.0
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
@@ -30,4 +30,4 @@ __all__ = [
30
30
  "PermisAPIValidationError",
31
31
  ]
32
32
 
33
- __version__ = "0.6.0"
33
+ __version__ = "0.7.0"
@@ -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.6.0"
16
+ USER_AGENT = "permisapi-client-python/0.7.0"
17
17
 
18
18
 
19
19
  def _raise_for_status(resp: httpx.Response) -> None:
@@ -177,6 +177,33 @@ class _PermitsResource:
177
177
  """
178
178
  return self._c._get(f"/v1/permits/{num_pa}/plu")
179
179
 
180
+ def full_view(self, num_pa: str) -> dict[str, Any]:
181
+ """GET /v1/permits/{num_pa}/360 : vue agregee 6-en-1 en 1 appel HTTP.
182
+
183
+ Combine en 1 reponse : detail + sirene + dvf + score + plu + risks.
184
+ Equivalent fonctionnel de 6 calls separes mais en 1 round-trip,
185
+ avec parallelisation cote serveur des sub-fetches lents (PLU
186
+ apicarto + Risks BRGM).
187
+
188
+ Cout quota : 6 unites du quota mensuel pour Pro+, 1 unite pour
189
+ Free / Explorer (qui recoivent juste `detail`, autres champs `null`
190
+ avec `plan_required_for_full="pro"`). Le header de reponse
191
+ `X-RateLimit-Cost` confirme le cout decompte.
192
+
193
+ Best-effort : si une sous-feature echoue (timeout, 5xx tiers),
194
+ son champ vaut `null` et l'erreur est listee dans `fetch_errors`.
195
+ Les autres sous-features ne sont pas affectees.
196
+
197
+ Latence typique 5-7s (dominee par PLU + Risks live, paralleles).
198
+
199
+ Returns:
200
+ dict avec cles : num_pa, detail, sirene, dvf, score, plu,
201
+ risks, fetch_errors (list), subfetches_billed (int).
202
+ Si plan Free/Explorer : `plan_required_for_full="pro"` est
203
+ present et toutes les sous-features sauf detail sont null.
204
+ """
205
+ return self._c._get(f"/v1/permits/{num_pa}/360")
206
+
180
207
  def score_feedback(
181
208
  self,
182
209
  *,
@@ -506,6 +533,15 @@ class _AsyncPermitsResource:
506
533
  Voir _PermitsResource.risks pour la doc complete."""
507
534
  return await self._c._get(f"/v1/permits/{num_pa}/risks")
508
535
 
536
+ async def full_view(self, num_pa: str) -> dict[str, Any]:
537
+ """GET /v1/permits/{num_pa}/360 : version async. Vue agregee 6-en-1.
538
+
539
+ Cout quota : 6 unites pour Pro+, 1 unite pour Free / Explorer.
540
+ Le header `X-RateLimit-Cost` confirme le cout. Voir
541
+ _PermitsResource.full_view pour la doc complete.
542
+ """
543
+ return await self._c._get(f"/v1/permits/{num_pa}/360")
544
+
509
545
  async def score_feedback(
510
546
  self,
511
547
  *,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "permisapi-client"
7
- version = "0.6.0"
7
+ version = "0.7.0"
8
8
  description = "Client Python officiel pour PermisAPI : permis de construire France en API REST."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -337,6 +337,56 @@ 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_full_view_sync() -> None:
342
+ """Sync : full_view appelle GET /v1/permits/{num_pa}/360 et retourne
343
+ le payload agrege complet."""
344
+ respx.get(f"{BASE}/v1/permits/PC-360-001/360").respond(
345
+ json={
346
+ "num_pa": "PC-360-001",
347
+ "detail": {"num_pa": "PC-360-001", "dep_code": "75"},
348
+ "sirene": {"match": {"siren": "111222333"}},
349
+ "dvf": {"matches": []},
350
+ "score": {"score": 72, "tier": "high"},
351
+ "plu": {"zonage": {"code": "UA", "constructible": True}},
352
+ "risks": {"risk_tier": "low"},
353
+ "fetch_errors": [],
354
+ "subfetches_billed": 6,
355
+ }
356
+ )
357
+ with PermisAPI(api_key="pk_test_abc") as client:
358
+ view = client.permits.full_view("PC-360-001")
359
+ assert view["num_pa"] == "PC-360-001"
360
+ assert view["subfetches_billed"] == 6
361
+ assert view["score"]["tier"] == "high"
362
+ assert view["plu"]["zonage"]["constructible"] is True
363
+
364
+
365
+ @pytest.mark.asyncio
366
+ @respx.mock
367
+ async def test_async_permits_full_view() -> None:
368
+ """Async : full_view appelle GET /v1/permits/{num_pa}/360."""
369
+ respx.get(f"{BASE}/v1/permits/PC-FREE/360").respond(
370
+ json={
371
+ "num_pa": "PC-FREE",
372
+ "detail": {"num_pa": "PC-FREE"},
373
+ "sirene": None,
374
+ "dvf": None,
375
+ "score": None,
376
+ "plu": None,
377
+ "risks": None,
378
+ "fetch_errors": [],
379
+ "plan_required_for_full": "pro",
380
+ "subfetches_billed": 1,
381
+ }
382
+ )
383
+ async with AsyncPermisAPI(api_key="pk_test_abc") as client:
384
+ view = await client.permits.full_view("PC-FREE")
385
+ assert view["plan_required_for_full"] == "pro"
386
+ assert view["subfetches_billed"] == 1
387
+ assert view["sirene"] is None
388
+
389
+
340
390
  def test_custom_base_url() -> None:
341
391
  client = PermisAPI(api_key="pk_test_abc", base_url="http://localhost:8000/")
342
392
  assert client.base_url == "http://localhost:8000"