permisapi-client 0.2.0__tar.gz → 0.3.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.3.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,54 @@ 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 export(
134
+ self,
135
+ *,
136
+ dep_code: str | None = None,
137
+ comm_code: str | None = None,
138
+ permit_type: str | None = None,
139
+ date_from: str | None = None,
140
+ date_to: str | None = None,
141
+ etat_pa: int | None = None,
142
+ max_rows: int | None = None,
143
+ ) -> bytes:
144
+ """GET /v1/permits/export : export streaming CSV bulk (Business+).
145
+
146
+ Plans Business et Enterprise uniquement (403 si plan inferieur).
147
+ Cap par defaut : 100k lignes Business, 1M Enterprise. Le user
148
+ peut reduire via `max_rows` mais pas augmenter.
149
+
150
+ Retourne le contenu CSV brut en bytes UTF-8 (header + rows). Pour
151
+ sauver dans un fichier :
152
+
153
+ csv_bytes = client.permits.export(dep_code="75")
154
+ with open("paris.csv", "wb") as f:
155
+ f.write(csv_bytes)
156
+
157
+ Pour les gros exports (>100 MB), prefere le streaming via
158
+ `client._http.stream("GET", "/v1/permits/export", params=...)`.
159
+ """
160
+ params: dict[str, Any] = {}
161
+ if dep_code is not None:
162
+ params["dep_code"] = dep_code
163
+ if comm_code is not None:
164
+ params["comm_code"] = comm_code
165
+ if permit_type is not None:
166
+ params["permit_type"] = permit_type
167
+ if date_from is not None:
168
+ params["date_from"] = date_from
169
+ if date_to is not None:
170
+ params["date_to"] = date_to
171
+ if etat_pa is not None:
172
+ params["etat_pa"] = etat_pa
173
+ if max_rows is not None:
174
+ params["max_rows"] = max_rows
175
+ # Sortie binaire (CSV) -> on contourne _get qui parse en JSON.
176
+ url = f"{self._c.base_url}/v1/permits/export"
177
+ resp = self._c._http.get(url, params=params or None)
178
+ _raise_for_status(resp)
179
+ return resp.content
180
+
133
181
 
134
182
  class _StatsResource:
135
183
  def __init__(self, client: PermisAPI) -> None:
@@ -357,6 +405,39 @@ class _AsyncPermitsResource:
357
405
  params["max_distance"] = max_distance
358
406
  return await self._c._get(f"/v1/permits/{num_pa}/dvf", params=params or None)
359
407
 
408
+ async def export(
409
+ self,
410
+ *,
411
+ dep_code: str | None = None,
412
+ comm_code: str | None = None,
413
+ permit_type: str | None = None,
414
+ date_from: str | None = None,
415
+ date_to: str | None = None,
416
+ etat_pa: int | None = None,
417
+ max_rows: int | None = None,
418
+ ) -> bytes:
419
+ """GET /v1/permits/export : version async. Plans Business+ only.
420
+ Voir _PermitsResource.export pour la doc complete des params."""
421
+ params: dict[str, Any] = {}
422
+ if dep_code is not None:
423
+ params["dep_code"] = dep_code
424
+ if comm_code is not None:
425
+ params["comm_code"] = comm_code
426
+ if permit_type is not None:
427
+ params["permit_type"] = permit_type
428
+ if date_from is not None:
429
+ params["date_from"] = date_from
430
+ if date_to is not None:
431
+ params["date_to"] = date_to
432
+ if etat_pa is not None:
433
+ params["etat_pa"] = etat_pa
434
+ if max_rows is not None:
435
+ params["max_rows"] = max_rows
436
+ url = f"{self._c.base_url}/v1/permits/export"
437
+ resp = await self._c._http.get(url, params=params or None)
438
+ _raise_for_status(resp)
439
+ return resp.content
440
+
360
441
 
361
442
  class _AsyncStatsResource:
362
443
  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.3.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,76 @@ 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
+ @pytest.mark.asyncio
306
+ @respx.mock
307
+ async def test_async_permits_export() -> None:
308
+ csv_body = b"num_pa,dep_code\nPC-A,75\n"
309
+ respx.get(f"{BASE}/v1/permits/export").respond(
310
+ content=csv_body, headers={"content-type": "text/csv"},
311
+ )
312
+ async with AsyncPermisAPI(api_key="pk_test_abc") as client:
313
+ result = await client.permits.export(dep_code="75")
314
+ assert isinstance(result, bytes)
315
+ assert b"PC-A,75" in result
316
+
317
+
248
318
  def test_custom_base_url() -> None:
249
319
  client = PermisAPI(api_key="pk_test_abc", base_url="http://localhost:8000/")
250
320
  assert client.base_url == "http://localhost:8000"