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.
- {permisapi_client-0.2.0 → permisapi_client-0.4.0}/PKG-INFO +1 -1
- {permisapi_client-0.2.0 → permisapi_client-0.4.0}/permisapi_client/client.py +99 -0
- {permisapi_client-0.2.0 → permisapi_client-0.4.0}/pyproject.toml +1 -1
- {permisapi_client-0.2.0 → permisapi_client-0.4.0}/tests/test_client.py +92 -0
- {permisapi_client-0.2.0 → permisapi_client-0.4.0}/.gitignore +0 -0
- {permisapi_client-0.2.0 → permisapi_client-0.4.0}/README.md +0 -0
- {permisapi_client-0.2.0 → permisapi_client-0.4.0}/permisapi_client/__init__.py +0 -0
- {permisapi_client-0.2.0 → permisapi_client-0.4.0}/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.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:
|
|
@@ -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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|