permisapi-client 0.2.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: permisapi-client
3
- Version: 0.2.0
3
+ Version: 0.4.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
@@ -130,6 +130,68 @@ class _PermitsResource:
130
130
  params["max_distance"] = max_distance
131
131
  return self._c._get(f"/v1/permits/{num_pa}/dvf", params=params or None)
132
132
 
133
+ def score_mdb(self, num_pa: str) -> dict[str, Any]:
134
+ """GET /v1/permits/{num_pa}/score : Score Opportunité MDB v0.1.
135
+
136
+ Plans Pro et superieurs uniquement (402 si Free / Explorer).
137
+ Retourne {num_pa, permit_id, score (0-100), tier (low/medium/
138
+ high/premium), components (7 signaux ponderes), weights,
139
+ version, method, dvf_matches_used, density_class}.
140
+
141
+ Algo : heuristique ponderee qui combine type permit, surface,
142
+ DVF voisin €/m², activite dep, demandeur (SCI vs promoteur),
143
+ coherence DVF/permit, densite INSEE.
144
+ """
145
+ return self._c._get(f"/v1/permits/{num_pa}/score")
146
+
147
+ def export(
148
+ self,
149
+ *,
150
+ dep_code: str | None = None,
151
+ comm_code: str | None = None,
152
+ permit_type: str | None = None,
153
+ date_from: str | None = None,
154
+ date_to: str | None = None,
155
+ etat_pa: int | None = None,
156
+ max_rows: int | None = None,
157
+ ) -> bytes:
158
+ """GET /v1/permits/export : export streaming CSV bulk (Business+).
159
+
160
+ Plans Business et Enterprise uniquement (403 si plan inferieur).
161
+ Cap par defaut : 100k lignes Business, 1M Enterprise. Le user
162
+ peut reduire via `max_rows` mais pas augmenter.
163
+
164
+ Retourne le contenu CSV brut en bytes UTF-8 (header + rows). Pour
165
+ sauver dans un fichier :
166
+
167
+ csv_bytes = client.permits.export(dep_code="75")
168
+ with open("paris.csv", "wb") as f:
169
+ f.write(csv_bytes)
170
+
171
+ Pour les gros exports (>100 MB), prefere le streaming via
172
+ `client._http.stream("GET", "/v1/permits/export", params=...)`.
173
+ """
174
+ params: dict[str, Any] = {}
175
+ if dep_code is not None:
176
+ params["dep_code"] = dep_code
177
+ if comm_code is not None:
178
+ params["comm_code"] = comm_code
179
+ if permit_type is not None:
180
+ params["permit_type"] = permit_type
181
+ if date_from is not None:
182
+ params["date_from"] = date_from
183
+ if date_to is not None:
184
+ params["date_to"] = date_to
185
+ if etat_pa is not None:
186
+ params["etat_pa"] = etat_pa
187
+ if max_rows is not None:
188
+ params["max_rows"] = max_rows
189
+ # Sortie binaire (CSV) -> on contourne _get qui parse en JSON.
190
+ url = f"{self._c.base_url}/v1/permits/export"
191
+ resp = self._c._http.get(url, params=params or None)
192
+ _raise_for_status(resp)
193
+ return resp.content
194
+
133
195
 
134
196
  class _StatsResource:
135
197
  def __init__(self, client: PermisAPI) -> None:
@@ -357,6 +419,43 @@ class _AsyncPermitsResource:
357
419
  params["max_distance"] = max_distance
358
420
  return await self._c._get(f"/v1/permits/{num_pa}/dvf", params=params or None)
359
421
 
422
+ async def score_mdb(self, num_pa: str) -> dict[str, Any]:
423
+ """GET /v1/permits/{num_pa}/score : version async. Plans Pro+ only."""
424
+ return await self._c._get(f"/v1/permits/{num_pa}/score")
425
+
426
+ async def export(
427
+ self,
428
+ *,
429
+ dep_code: str | None = None,
430
+ comm_code: str | None = None,
431
+ permit_type: str | None = None,
432
+ date_from: str | None = None,
433
+ date_to: str | None = None,
434
+ etat_pa: int | None = None,
435
+ max_rows: int | None = None,
436
+ ) -> bytes:
437
+ """GET /v1/permits/export : version async. Plans Business+ only.
438
+ Voir _PermitsResource.export pour la doc complete des params."""
439
+ params: dict[str, Any] = {}
440
+ if dep_code is not None:
441
+ params["dep_code"] = dep_code
442
+ if comm_code is not None:
443
+ params["comm_code"] = comm_code
444
+ if permit_type is not None:
445
+ params["permit_type"] = permit_type
446
+ if date_from is not None:
447
+ params["date_from"] = date_from
448
+ if date_to is not None:
449
+ params["date_to"] = date_to
450
+ if etat_pa is not None:
451
+ params["etat_pa"] = etat_pa
452
+ if max_rows is not None:
453
+ params["max_rows"] = max_rows
454
+ url = f"{self._c.base_url}/v1/permits/export"
455
+ resp = await self._c._http.get(url, params=params or None)
456
+ _raise_for_status(resp)
457
+ return resp.content
458
+
360
459
 
361
460
  class _AsyncStatsResource:
362
461
  def __init__(self, client: AsyncPermisAPI) -> None:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "permisapi-client"
7
- version = "0.2.0"
7
+ version = "0.4.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"
@@ -245,6 +245,98 @@ async def test_async_permits_dvf() -> None:
245
245
  assert result["num_pa"] == "PC-DVF-A"
246
246
 
247
247
 
248
+ # ---------------------------------------------------------------------------
249
+ # Export bulk CSV (Business+)
250
+ # ---------------------------------------------------------------------------
251
+
252
+
253
+ @respx.mock
254
+ def test_permits_export_returns_csv_bytes() -> None:
255
+ """client.permits.export() retourne le CSV brut en bytes."""
256
+ csv_body = (
257
+ b"num_pa,dep_code,comm_code,permit_type\n"
258
+ b"PC-1,75,75118,PC_LOGEMENT\n"
259
+ b"PC-2,75,75118,PC_LOCAUX\n"
260
+ )
261
+ respx.get(f"{BASE}/v1/permits/export").respond(
262
+ content=csv_body,
263
+ headers={"content-type": "text/csv"},
264
+ )
265
+ with PermisAPI(api_key="pk_test_abc") as client:
266
+ result = client.permits.export(dep_code="75")
267
+ assert isinstance(result, bytes)
268
+ assert b"PC-1,75" in result
269
+ assert result.startswith(b"num_pa,")
270
+
271
+
272
+ @respx.mock
273
+ def test_permits_export_passes_filters_as_query() -> None:
274
+ route = respx.get(f"{BASE}/v1/permits/export").respond(content=b"num_pa\n")
275
+ with PermisAPI(api_key="pk_test_abc") as client:
276
+ client.permits.export(
277
+ dep_code="75",
278
+ comm_code="75118",
279
+ permit_type="PC_LOGEMENT",
280
+ date_from="2024-01-01",
281
+ date_to="2024-12-31",
282
+ etat_pa=1,
283
+ max_rows=5000,
284
+ )
285
+ qs = dict(route.calls.last.request.url.params)
286
+ assert qs["dep_code"] == "75"
287
+ assert qs["comm_code"] == "75118"
288
+ assert qs["permit_type"] == "PC_LOGEMENT"
289
+ assert qs["date_from"] == "2024-01-01"
290
+ assert qs["max_rows"] == "5000"
291
+
292
+
293
+ @respx.mock
294
+ def test_permits_export_403_for_explorer_plan() -> None:
295
+ """Plan inferieur a Business -> 403 Forbidden remonte tel quel."""
296
+ respx.get(f"{BASE}/v1/permits/export").respond(
297
+ status_code=403,
298
+ json={"error": "forbidden", "detail": "Plan Business+ requis"},
299
+ )
300
+ with PermisAPI(api_key="pk_test_abc") as client:
301
+ with pytest.raises(Exception):
302
+ client.permits.export()
303
+
304
+
305
+ @respx.mock
306
+ def test_permits_score_mdb() -> None:
307
+ respx.get(f"{BASE}/v1/permits/PC-1/score").respond(
308
+ json={"num_pa": "PC-1", "score": 78, "tier": "high", "components": {}}
309
+ )
310
+ with PermisAPI(api_key="pk_test_abc") as client:
311
+ result = client.permits.score_mdb("PC-1")
312
+ assert result["score"] == 78
313
+ assert result["tier"] == "high"
314
+
315
+
316
+ @pytest.mark.asyncio
317
+ @respx.mock
318
+ async def test_async_permits_score_mdb() -> None:
319
+ respx.get(f"{BASE}/v1/permits/PC-A/score").respond(
320
+ json={"num_pa": "PC-A", "score": 42, "tier": "medium"}
321
+ )
322
+ async with AsyncPermisAPI(api_key="pk_test_abc") as client:
323
+ result = await client.permits.score_mdb("PC-A")
324
+ assert result["score"] == 42
325
+
326
+
327
+ @pytest.mark.asyncio
328
+ @respx.mock
329
+ async def test_async_permits_export() -> None:
330
+ csv_body = b"num_pa,dep_code\nPC-A,75\n"
331
+ respx.get(f"{BASE}/v1/permits/export").respond(
332
+ content=csv_body, headers={"content-type": "text/csv"},
333
+ )
334
+ async with AsyncPermisAPI(api_key="pk_test_abc") as client:
335
+ result = await client.permits.export(dep_code="75")
336
+ assert isinstance(result, bytes)
337
+ assert b"PC-A,75" in result
338
+
339
+
248
340
  def test_custom_base_url() -> None:
249
341
  client = PermisAPI(api_key="pk_test_abc", base_url="http://localhost:8000/")
250
342
  assert client.base_url == "http://localhost:8000"