permisapi-client 0.8.1__tar.gz → 0.9.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.8.1 → permisapi_client-0.9.0}/PKG-INFO +27 -14
- {permisapi_client-0.8.1 → permisapi_client-0.9.0}/README.md +26 -13
- {permisapi_client-0.8.1 → permisapi_client-0.9.0}/permisapi_client/__init__.py +1 -1
- {permisapi_client-0.8.1 → permisapi_client-0.9.0}/permisapi_client/client.py +104 -66
- {permisapi_client-0.8.1 → permisapi_client-0.9.0}/permisapi_client/exceptions.py +1 -1
- {permisapi_client-0.8.1 → permisapi_client-0.9.0}/pyproject.toml +1 -1
- {permisapi_client-0.8.1 → permisapi_client-0.9.0}/tests/test_client.py +31 -0
- {permisapi_client-0.8.1 → permisapi_client-0.9.0}/.gitignore +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: permisapi-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Client Python officiel pour PermisAPI : permis de construire de France en API REST.
|
|
5
5
|
Project-URL: Homepage, https://permisapi.fr
|
|
6
6
|
Project-URL: Documentation, https://permisapi.fr/docs
|
|
@@ -57,6 +57,13 @@ nearby = client.permits.near(lat=48.8599, lng=2.3423, radius_m=300)
|
|
|
57
57
|
# Détail d'un permis
|
|
58
58
|
permit = client.permits.get("07510125V0025")
|
|
59
59
|
|
|
60
|
+
# Un même numéro peut porter plusieurs autorisations (un PC et un PD, par
|
|
61
|
+
# exemple) : la fiche liste les autres types dans autres_types, et type=
|
|
62
|
+
# en désigne une. permit["identifiant"] (type et numéro accolés) est la
|
|
63
|
+
# forme à conserver ; elle est acceptée partout à la place du numéro.
|
|
64
|
+
if permit.get("autres_types"):
|
|
65
|
+
autre = client.permits.get(permit["num_pa"], type=permit["autres_types"][0])
|
|
66
|
+
|
|
60
67
|
# Statistiques d'une commune (Paris entier est rangé sous le code 75056 dans Sitadel)
|
|
61
68
|
stats = client.stats.commune("75056")
|
|
62
69
|
print(stats["total_permits"], stats["granted"], stats["trend_yoy_pct"])
|
|
@@ -68,16 +75,17 @@ for p in client.permits.iter_all(dep_code="75", permit_type="PC_LOGEMENT"):
|
|
|
68
75
|
|
|
69
76
|
Coût : une recherche (`list`, `iter_all`, `near`, `search`) décompte 1 requête par permis
|
|
70
77
|
renvoyé. `iter_all` sur un département entier peut donc épuiser le quota mensuel de la
|
|
71
|
-
formule Gratuit (500 requêtes) ou
|
|
78
|
+
formule Gratuit (500 requêtes), Découverte (2 000) ou Explorateur (5 000).
|
|
72
79
|
|
|
73
|
-
Formules : **Gratuit** (1 département,
|
|
74
|
-
6 mois), **
|
|
75
|
-
(
|
|
80
|
+
Formules : **Gratuit** (1 département, les nouveaux permis des 2 dernières publications
|
|
81
|
+
Sitadel), **Découverte** (1 département, 6 mois), **Explorateur** (5 départements, 12 mois),
|
|
82
|
+
**Régional** (5 départements, historique complet et données avancées), **Pro** (les mêmes
|
|
83
|
+
données avancées sur la France entière), **Business** et **Entreprise**. Les
|
|
76
84
|
exemples de cette page portent sur Paris (75).
|
|
77
85
|
|
|
78
|
-
## Données avancées (formules Régional, Pro, Business et
|
|
86
|
+
## Données avancées (formules Régional, Pro, Business et Entreprise)
|
|
79
87
|
|
|
80
|
-
Les formules Gratuit et
|
|
88
|
+
Les formules Gratuit, Découverte et Explorateur reçoivent une erreur 402 sur ces routes.
|
|
81
89
|
|
|
82
90
|
```python
|
|
83
91
|
num_pa = "07510125V0025"
|
|
@@ -91,8 +99,8 @@ print("Prix médian au m² :", dvf["dvf_median_price_per_m2"])
|
|
|
91
99
|
# Score d'opportunité marchand de biens v0.3 (note 0-100 et palier)
|
|
92
100
|
score = client.permits.score_mdb(num_pa)
|
|
93
101
|
print(f"Score : {score['score']}/100 ({score['tier']})")
|
|
94
|
-
#
|
|
95
|
-
#
|
|
102
|
+
# score["components"] : note 0-100 de chacun des 11 signaux ; une explication
|
|
103
|
+
# en français de chaque signal : route /v1/permits/{num_pa}/score/explain.
|
|
96
104
|
|
|
97
105
|
# Zonage PLU (constructible, à urbaniser, agricole, naturel)
|
|
98
106
|
plu = client.permits.plu(num_pa)
|
|
@@ -102,9 +110,9 @@ if plu["has_plu"]:
|
|
|
102
110
|
|
|
103
111
|
# Risques Géorisques (inondation, séisme, sites Seveso proches...)
|
|
104
112
|
risks = client.permits.risks(num_pa)
|
|
105
|
-
print(
|
|
106
|
-
for
|
|
107
|
-
print(
|
|
113
|
+
print(risks["risk_summary"]) # niveau, et ce qu'il mesure
|
|
114
|
+
for raison in risks["risk_reasons"]: # d'où viennent les points
|
|
115
|
+
print("-", raison)
|
|
108
116
|
|
|
109
117
|
# Votre avis sur un score
|
|
110
118
|
client.permits.score_feedback(
|
|
@@ -117,10 +125,10 @@ client.permits.score_feedback(
|
|
|
117
125
|
)
|
|
118
126
|
```
|
|
119
127
|
|
|
120
|
-
## Export CSV (Business et
|
|
128
|
+
## Export CSV (Business et Entreprise)
|
|
121
129
|
|
|
122
130
|
```python
|
|
123
|
-
# Plafond : 100 000 lignes en Business, 1 000 000 en
|
|
131
|
+
# Plafond : 100 000 lignes en Business, 1 000 000 en Entreprise
|
|
124
132
|
csv_bytes = client.permits.export(
|
|
125
133
|
dep_code="75",
|
|
126
134
|
permit_type="PC_LOGEMENT",
|
|
@@ -196,6 +204,11 @@ except PermisAPIRateLimited as e:
|
|
|
196
204
|
print(f"Trop de requêtes par minute, nouvel essai possible dans {e.retry_after} s")
|
|
197
205
|
```
|
|
198
206
|
|
|
207
|
+
## Journal des versions
|
|
208
|
+
|
|
209
|
+
- **0.9.0** (12/11/2026, bascule de la clé type et numéro, C24) : argument facultatif `type` (`PC`, `DP`, `PA` ou `PD`) sur les méthodes qui prennent un `num_pa` ; `identifiant` (`PC0330632600176`) accepté partout à la place du numéro ; l'export CSV gagne `type_autorisation` et `identifiant` en fin de ligne. Version mineure : ajouts seulement, aucun appel existant ne change.
|
|
210
|
+
- Versions précédentes : historique du dépôt (`git log -- sdk-python/`).
|
|
211
|
+
|
|
199
212
|
## Licence
|
|
200
213
|
|
|
201
214
|
MIT. Source des permis : SDES, base Sitadel, data.gouv.fr (Licence Ouverte Etalab).
|
|
@@ -29,6 +29,13 @@ nearby = client.permits.near(lat=48.8599, lng=2.3423, radius_m=300)
|
|
|
29
29
|
# Détail d'un permis
|
|
30
30
|
permit = client.permits.get("07510125V0025")
|
|
31
31
|
|
|
32
|
+
# Un même numéro peut porter plusieurs autorisations (un PC et un PD, par
|
|
33
|
+
# exemple) : la fiche liste les autres types dans autres_types, et type=
|
|
34
|
+
# en désigne une. permit["identifiant"] (type et numéro accolés) est la
|
|
35
|
+
# forme à conserver ; elle est acceptée partout à la place du numéro.
|
|
36
|
+
if permit.get("autres_types"):
|
|
37
|
+
autre = client.permits.get(permit["num_pa"], type=permit["autres_types"][0])
|
|
38
|
+
|
|
32
39
|
# Statistiques d'une commune (Paris entier est rangé sous le code 75056 dans Sitadel)
|
|
33
40
|
stats = client.stats.commune("75056")
|
|
34
41
|
print(stats["total_permits"], stats["granted"], stats["trend_yoy_pct"])
|
|
@@ -40,16 +47,17 @@ for p in client.permits.iter_all(dep_code="75", permit_type="PC_LOGEMENT"):
|
|
|
40
47
|
|
|
41
48
|
Coût : une recherche (`list`, `iter_all`, `near`, `search`) décompte 1 requête par permis
|
|
42
49
|
renvoyé. `iter_all` sur un département entier peut donc épuiser le quota mensuel de la
|
|
43
|
-
formule Gratuit (500 requêtes) ou
|
|
50
|
+
formule Gratuit (500 requêtes), Découverte (2 000) ou Explorateur (5 000).
|
|
44
51
|
|
|
45
|
-
Formules : **Gratuit** (1 département,
|
|
46
|
-
6 mois), **
|
|
47
|
-
(
|
|
52
|
+
Formules : **Gratuit** (1 département, les nouveaux permis des 2 dernières publications
|
|
53
|
+
Sitadel), **Découverte** (1 département, 6 mois), **Explorateur** (5 départements, 12 mois),
|
|
54
|
+
**Régional** (5 départements, historique complet et données avancées), **Pro** (les mêmes
|
|
55
|
+
données avancées sur la France entière), **Business** et **Entreprise**. Les
|
|
48
56
|
exemples de cette page portent sur Paris (75).
|
|
49
57
|
|
|
50
|
-
## Données avancées (formules Régional, Pro, Business et
|
|
58
|
+
## Données avancées (formules Régional, Pro, Business et Entreprise)
|
|
51
59
|
|
|
52
|
-
Les formules Gratuit et
|
|
60
|
+
Les formules Gratuit, Découverte et Explorateur reçoivent une erreur 402 sur ces routes.
|
|
53
61
|
|
|
54
62
|
```python
|
|
55
63
|
num_pa = "07510125V0025"
|
|
@@ -63,8 +71,8 @@ print("Prix médian au m² :", dvf["dvf_median_price_per_m2"])
|
|
|
63
71
|
# Score d'opportunité marchand de biens v0.3 (note 0-100 et palier)
|
|
64
72
|
score = client.permits.score_mdb(num_pa)
|
|
65
73
|
print(f"Score : {score['score']}/100 ({score['tier']})")
|
|
66
|
-
#
|
|
67
|
-
#
|
|
74
|
+
# score["components"] : note 0-100 de chacun des 11 signaux ; une explication
|
|
75
|
+
# en français de chaque signal : route /v1/permits/{num_pa}/score/explain.
|
|
68
76
|
|
|
69
77
|
# Zonage PLU (constructible, à urbaniser, agricole, naturel)
|
|
70
78
|
plu = client.permits.plu(num_pa)
|
|
@@ -74,9 +82,9 @@ if plu["has_plu"]:
|
|
|
74
82
|
|
|
75
83
|
# Risques Géorisques (inondation, séisme, sites Seveso proches...)
|
|
76
84
|
risks = client.permits.risks(num_pa)
|
|
77
|
-
print(
|
|
78
|
-
for
|
|
79
|
-
print(
|
|
85
|
+
print(risks["risk_summary"]) # niveau, et ce qu'il mesure
|
|
86
|
+
for raison in risks["risk_reasons"]: # d'où viennent les points
|
|
87
|
+
print("-", raison)
|
|
80
88
|
|
|
81
89
|
# Votre avis sur un score
|
|
82
90
|
client.permits.score_feedback(
|
|
@@ -89,10 +97,10 @@ client.permits.score_feedback(
|
|
|
89
97
|
)
|
|
90
98
|
```
|
|
91
99
|
|
|
92
|
-
## Export CSV (Business et
|
|
100
|
+
## Export CSV (Business et Entreprise)
|
|
93
101
|
|
|
94
102
|
```python
|
|
95
|
-
# Plafond : 100 000 lignes en Business, 1 000 000 en
|
|
103
|
+
# Plafond : 100 000 lignes en Business, 1 000 000 en Entreprise
|
|
96
104
|
csv_bytes = client.permits.export(
|
|
97
105
|
dep_code="75",
|
|
98
106
|
permit_type="PC_LOGEMENT",
|
|
@@ -168,6 +176,11 @@ except PermisAPIRateLimited as e:
|
|
|
168
176
|
print(f"Trop de requêtes par minute, nouvel essai possible dans {e.retry_after} s")
|
|
169
177
|
```
|
|
170
178
|
|
|
179
|
+
## Journal des versions
|
|
180
|
+
|
|
181
|
+
- **0.9.0** (12/11/2026, bascule de la clé type et numéro, C24) : argument facultatif `type` (`PC`, `DP`, `PA` ou `PD`) sur les méthodes qui prennent un `num_pa` ; `identifiant` (`PC0330632600176`) accepté partout à la place du numéro ; l'export CSV gagne `type_autorisation` et `identifiant` en fin de ligne. Version mineure : ajouts seulement, aucun appel existant ne change.
|
|
182
|
+
- Versions précédentes : historique du dépôt (`git log -- sdk-python/`).
|
|
183
|
+
|
|
171
184
|
## Licence
|
|
172
185
|
|
|
173
186
|
MIT. Source des permis : SDES, base Sitadel, data.gouv.fr (Licence Ouverte Etalab).
|
|
@@ -13,7 +13,25 @@ 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.9.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
TYPES_AUTORISATION = ("PC", "DP", "PA", "PD")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _params_type(type_: str | None) -> dict[str, str] | None:
|
|
23
|
+
"""Paramètre facultatif `type` des méthodes à `num_pa` (API, 05/11/2026) :
|
|
24
|
+
un même numéro peut porter plusieurs autorisations (un PC et un PD, par
|
|
25
|
+
exemple) ; `type="PD"` désigne laquelle. Sans lui, l'autorisation servie
|
|
26
|
+
par défaut (toujours la même pour un numéro), comme avant. Un type que ce
|
|
27
|
+
numéro ne porte pas donne une erreur 404. Même effet : passer
|
|
28
|
+
l'`identifiant` (`PD0330632600176`) à la place du numéro."""
|
|
29
|
+
if type_ is None or type_ == "":
|
|
30
|
+
return None
|
|
31
|
+
t = str(type_).strip().upper()
|
|
32
|
+
if t not in TYPES_AUTORISATION:
|
|
33
|
+
raise ValueError("type vaut PC, DP, PA ou PD")
|
|
34
|
+
return {"type": t}
|
|
17
35
|
|
|
18
36
|
|
|
19
37
|
def _raise_for_status(resp: httpx.Response) -> None:
|
|
@@ -117,7 +135,7 @@ class _PermitsResource:
|
|
|
117
135
|
6 travaux terminés (1 et 3 n'existent pas dans Sitadel).
|
|
118
136
|
|
|
119
137
|
Filtre `min_score` (formules Régional, Pro, Business et
|
|
120
|
-
|
|
138
|
+
Entreprise ; 402 pour Gratuit, Découverte et Explorateur) : retourne uniquement
|
|
121
139
|
les permis dont le Score Opportunité MDB est >= N (0-100). Cas
|
|
122
140
|
d'usage marchand de biens : présélectionner les meilleures
|
|
123
141
|
opportunités en un appel au lieu de filtrer côté client.
|
|
@@ -146,13 +164,13 @@ class _PermitsResource:
|
|
|
146
164
|
|
|
147
165
|
Attention au quota : chaque permis retourné coûte 1 unité.
|
|
148
166
|
Itérer sur un département entier peut consommer tout le quota
|
|
149
|
-
mensuel d'une formule Gratuit (500) ou
|
|
167
|
+
mensuel d'une formule Gratuit (500), Découverte (2 000) ou Explorateur (5 000).
|
|
150
168
|
"""
|
|
151
169
|
yield from _iter_pages(self.list, params)
|
|
152
170
|
|
|
153
|
-
def get(self, num_pa: str) -> dict[str, Any]:
|
|
171
|
+
def get(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
154
172
|
"""GET /v1/permits/{num_pa}."""
|
|
155
|
-
return self._c._get(f"/v1/permits/{num_pa}")
|
|
173
|
+
return self._c._get(f"/v1/permits/{num_pa}", params=_params_type(type))
|
|
156
174
|
|
|
157
175
|
def near(
|
|
158
176
|
self, *, lat: float, lng: float, radius_m: int, **filters: Any
|
|
@@ -171,17 +189,17 @@ class _PermitsResource:
|
|
|
171
189
|
def by_cadastre(self, *, section: str, numero: str) -> list[dict[str, Any]]:
|
|
172
190
|
"""GET /v1/permits/by-cadastre : liste nue des permis qui citent
|
|
173
191
|
cette parcelle (section + numéro). Formules Régional et plus
|
|
174
|
-
(403 pour Gratuit et
|
|
192
|
+
(403 pour Gratuit, Découverte et Explorateur)."""
|
|
175
193
|
return self._c._get(
|
|
176
194
|
"/v1/permits/by-cadastre",
|
|
177
195
|
params={"section": section, "numero": numero},
|
|
178
196
|
)
|
|
179
197
|
|
|
180
|
-
def parcelle(self, num_pa: str) -> dict[str, Any]:
|
|
198
|
+
def parcelle(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
181
199
|
"""GET /v1/permits/{num_pa}/parcelle : geometrie cadastre DGFiP.
|
|
182
200
|
|
|
183
|
-
Formules Régional, Pro, Business et
|
|
184
|
-
et
|
|
201
|
+
Formules Régional, Pro, Business et Entreprise (402 pour Gratuit,
|
|
202
|
+
Découverte et Explorateur).
|
|
185
203
|
Retourne {num_pa, comm_code, section, numero, cadastre_id,
|
|
186
204
|
surface_m2 (contenance officielle DGFiP), geometry (GeoJSON
|
|
187
205
|
Polygon WGS84), cached_at, source}.
|
|
@@ -194,13 +212,13 @@ class _PermitsResource:
|
|
|
194
212
|
un point lat/lng), comparer surface DGFiP vs Sitadel, cross-ref
|
|
195
213
|
avec DVF en parcelle precise.
|
|
196
214
|
"""
|
|
197
|
-
return self._c._get(f"/v1/permits/{num_pa}/parcelle")
|
|
215
|
+
return self._c._get(f"/v1/permits/{num_pa}/parcelle", params=_params_type(type))
|
|
198
216
|
|
|
199
|
-
def batiments_existants(self, num_pa: str) -> dict[str, Any]:
|
|
217
|
+
def batiments_existants(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
200
218
|
"""GET /v1/permits/{num_pa}/batiments-existants : batiments
|
|
201
219
|
cadastraux deja construits sur la parcelle du permit.
|
|
202
220
|
|
|
203
|
-
Formules Régional et plus (402 pour Gratuit et
|
|
221
|
+
Formules Régional et plus (402 pour Gratuit, Découverte et Explorateur).
|
|
204
222
|
Retourne :
|
|
205
223
|
- num_pa, parcelle_cadastre_id, parcelle_surface_m2
|
|
206
224
|
- nb_batiments (total), nb_bati_dur (type 01), nb_bati_leger
|
|
@@ -224,7 +242,7 @@ class _PermitsResource:
|
|
|
224
242
|
(matching spatial impossible). Source : cadastre.data.gouv.fr
|
|
225
243
|
(DGFiP via Etalab).
|
|
226
244
|
"""
|
|
227
|
-
return self._c._get(f"/v1/permits/{num_pa}/batiments-existants")
|
|
245
|
+
return self._c._get(f"/v1/permits/{num_pa}/batiments-existants", params=_params_type(type))
|
|
228
246
|
|
|
229
247
|
def bulk_enrich(
|
|
230
248
|
self,
|
|
@@ -247,8 +265,8 @@ class _PermitsResource:
|
|
|
247
265
|
|
|
248
266
|
`ref` est echo dans la response pour mapper sur votre data.
|
|
249
267
|
|
|
250
|
-
Formules Business et
|
|
251
|
-
|
|
268
|
+
Formules Business et Entreprise uniquement (402 pour Gratuit,
|
|
269
|
+
Découverte, Explorateur, Régional et Pro).
|
|
252
270
|
|
|
253
271
|
Args:
|
|
254
272
|
rows: liste max 1000 lignes
|
|
@@ -272,6 +290,7 @@ class _PermitsResource:
|
|
|
272
290
|
self,
|
|
273
291
|
num_pa: str,
|
|
274
292
|
*,
|
|
293
|
+
type: str | None = None,
|
|
275
294
|
limit: int | None = None,
|
|
276
295
|
type_local: str | None = None,
|
|
277
296
|
include_terrains: bool | None = None,
|
|
@@ -281,10 +300,15 @@ class _PermitsResource:
|
|
|
281
300
|
) -> dict[str, Any]:
|
|
282
301
|
"""GET /v1/permits/{num_pa}/dvf : top-K transactions DVF voisines.
|
|
283
302
|
|
|
284
|
-
Formules Régional et plus (402 pour Gratuit et
|
|
303
|
+
Formules Régional et plus (402 pour Gratuit, Découverte et Explorateur).
|
|
285
304
|
Retourne {num_pa, permit_id, matches: [...],
|
|
286
|
-
dvf_median_price_per_m2, dvf_median_sample_size,
|
|
287
|
-
source, coverage}. Ventes DVF de 2021 à 2025.
|
|
305
|
+
dvf_median_price_per_m2, dvf_median_sample_size, dvf_median_basis,
|
|
306
|
+
filters_applied, source, coverage}. Ventes DVF de 2021 à 2025.
|
|
307
|
+
|
|
308
|
+
Le prix médian au m² est le même que dans `/360` et `score_mdb()` :
|
|
309
|
+
il porte sur les 5 ventes voisines retenues par le score (au moins
|
|
310
|
+
1 000 €, surface bâtie de 10 à 1 000 m²), quels que soient `limit`
|
|
311
|
+
et les filtres, qui ne changent que `matches`.
|
|
288
312
|
|
|
289
313
|
Filtres optionnels :
|
|
290
314
|
- limit : 1-5, default 3
|
|
@@ -307,45 +331,55 @@ class _PermitsResource:
|
|
|
307
331
|
params["min_year"] = min_year
|
|
308
332
|
if max_distance is not None:
|
|
309
333
|
params["max_distance"] = max_distance
|
|
310
|
-
return self._c._get(
|
|
334
|
+
return self._c._get(
|
|
335
|
+
f"/v1/permits/{num_pa}/dvf",
|
|
336
|
+
params={**params, **(_params_type(type) or {})} or None,
|
|
337
|
+
)
|
|
311
338
|
|
|
312
|
-
def score_mdb(self, num_pa: str) -> dict[str, Any]:
|
|
339
|
+
def score_mdb(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
313
340
|
"""GET /v1/permits/{num_pa}/score : Score Opportunité MDB v0.3.
|
|
314
341
|
|
|
315
|
-
Formules Régional et plus (402 pour Gratuit et
|
|
342
|
+
Formules Régional et plus (402 pour Gratuit, Découverte et Explorateur).
|
|
316
343
|
Retourne {num_pa, permit_id, score (0-100), tier (low / medium /
|
|
317
344
|
high / premium), components, weights, version, method}.
|
|
318
345
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
`
|
|
325
|
-
|
|
346
|
+
`components` donne la note 0-100 de chacun des 11 signaux (type,
|
|
347
|
+
surface, dvf_value, dep_activity, demandeur, dvf_coherence,
|
|
348
|
+
density, plu, risk, sirene, building_density) et `weights` leur
|
|
349
|
+
poids ; la réponse ajoute `dvf_matches_used`,
|
|
350
|
+
`dvf_median_price_per_m2`, `dvf_median_sample_size` et
|
|
351
|
+
`density_class`. Une explication en français de chaque signal est
|
|
352
|
+
donnée par `GET /v1/permits/{num_pa}/score/explain`.
|
|
326
353
|
"""
|
|
327
|
-
return self._c._get(f"/v1/permits/{num_pa}/score")
|
|
354
|
+
return self._c._get(f"/v1/permits/{num_pa}/score", params=_params_type(type))
|
|
328
355
|
|
|
329
|
-
def risks(self, num_pa: str) -> dict[str, Any]:
|
|
356
|
+
def risks(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
330
357
|
"""GET /v1/permits/{num_pa}/risks : Georisques BRGM (commune-level).
|
|
331
358
|
|
|
332
|
-
Formules Régional et plus (402 pour Gratuit et
|
|
359
|
+
Formules Régional et plus (402 pour Gratuit, Découverte et Explorateur). Source :
|
|
333
360
|
API Géorisques (ministère de la Transition écologique et BRGM).
|
|
334
361
|
|
|
335
362
|
Retourne {num_pa, comm_code, risk_score (0-100), risk_tier (low/
|
|
336
|
-
moderate/high/critical),
|
|
337
|
-
|
|
338
|
-
|
|
363
|
+
moderate/high/critical), risk_tier_label (faible, modéré, élevé,
|
|
364
|
+
critique), risk_summary, risk_reasons, risks: [{code, label,
|
|
365
|
+
has_ppr, ppr_type}], icpe_proches: [{nom, regime, distance_m,
|
|
366
|
+
activite}], fetch_errors, source, fetched_at, disclaimer}.
|
|
367
|
+
|
|
368
|
+
`risk_reasons` dit d'où viennent les points (un risque recensé pour
|
|
369
|
+
la commune, les sites Seveso à moins de 2 km) ; `has_ppr` : True si
|
|
370
|
+
la commune a un plan de prévention opposable sur ce risque (archive
|
|
371
|
+
GASPAR, `ppr_lu_le`, `ppr_type`, `ppr_nom` ; points x1,5), False
|
|
372
|
+
sinon, None si non vérifié.
|
|
339
373
|
|
|
340
374
|
En cas d'erreur partielle (ex: ICPE OK mais /risques 500), retourne
|
|
341
375
|
ce qui marche + liste les endpoints qui ont fail dans `fetch_errors`.
|
|
342
376
|
"""
|
|
343
|
-
return self._c._get(f"/v1/permits/{num_pa}/risks")
|
|
377
|
+
return self._c._get(f"/v1/permits/{num_pa}/risks", params=_params_type(type))
|
|
344
378
|
|
|
345
|
-
def plu(self, num_pa: str) -> dict[str, Any]:
|
|
379
|
+
def plu(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
346
380
|
"""GET /v1/permits/{num_pa}/plu : zonage urbanisme PLU au point geocode.
|
|
347
381
|
|
|
348
|
-
Formules Régional et plus (402 pour Gratuit et
|
|
382
|
+
Formules Régional et plus (402 pour Gratuit, Découverte et Explorateur). Source :
|
|
349
383
|
Géoportail de l'Urbanisme via apicarto.ign.fr.
|
|
350
384
|
|
|
351
385
|
Retourne {num_pa, lat, lng, has_plu, zonage : {code (UA/UB/AU/A/N..),
|
|
@@ -355,9 +389,9 @@ class _PermitsResource:
|
|
|
355
389
|
Si la commune n'a pas de PLU numérisé sur le Géoportail,
|
|
356
390
|
`has_plu=False` avec `not_covered_reason` explicite.
|
|
357
391
|
"""
|
|
358
|
-
return self._c._get(f"/v1/permits/{num_pa}/plu")
|
|
392
|
+
return self._c._get(f"/v1/permits/{num_pa}/plu", params=_params_type(type))
|
|
359
393
|
|
|
360
|
-
def full_view(self, num_pa: str) -> dict[str, Any]:
|
|
394
|
+
def full_view(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
361
395
|
"""GET /v1/permits/{num_pa}/360 : vue agregee 6-en-1 en 1 appel HTTP.
|
|
362
396
|
|
|
363
397
|
Combine en 1 reponse : detail + sirene + dvf + score + plu + risks.
|
|
@@ -366,7 +400,7 @@ class _PermitsResource:
|
|
|
366
400
|
apicarto + Risks BRGM).
|
|
367
401
|
|
|
368
402
|
Coût quota : 6 unités pour les formules Régional, Pro, Business
|
|
369
|
-
et
|
|
403
|
+
et Entreprise ; 1 unité pour Gratuit, Découverte et Explorateur, qui reçoivent
|
|
370
404
|
seulement `detail` (autres champs `None`, et
|
|
371
405
|
`plan_required_for_full="pro"` alors que Régional suffit).
|
|
372
406
|
|
|
@@ -379,10 +413,10 @@ class _PermitsResource:
|
|
|
379
413
|
Returns:
|
|
380
414
|
dict avec cles : num_pa, detail, sirene, dvf, score, plu,
|
|
381
415
|
risks, fetch_errors (list), subfetches_billed (int).
|
|
382
|
-
Pour Gratuit et
|
|
416
|
+
Pour Gratuit, Découverte et Explorateur : `plan_required_for_full` est
|
|
383
417
|
présent et toutes les sous-parties sauf `detail` sont None.
|
|
384
418
|
"""
|
|
385
|
-
return self._c._get(f"/v1/permits/{num_pa}/360")
|
|
419
|
+
return self._c._get(f"/v1/permits/{num_pa}/360", params=_params_type(type))
|
|
386
420
|
|
|
387
421
|
def score_feedback(
|
|
388
422
|
self,
|
|
@@ -397,7 +431,7 @@ class _PermitsResource:
|
|
|
397
431
|
) -> dict[str, Any]:
|
|
398
432
|
"""POST /v1/score/feedback : envoyer un feedback sur un Score MDB.
|
|
399
433
|
|
|
400
|
-
Formules Régional et plus (402 pour Gratuit et
|
|
434
|
+
Formules Régional et plus (402 pour Gratuit, Découverte et Explorateur). Le
|
|
401
435
|
retour est enregistré et transmis par e-mail à l'équipe.
|
|
402
436
|
|
|
403
437
|
Args:
|
|
@@ -435,10 +469,10 @@ class _PermitsResource:
|
|
|
435
469
|
etat_pa: int | None = None,
|
|
436
470
|
max_rows: int | None = None,
|
|
437
471
|
) -> bytes:
|
|
438
|
-
"""GET /v1/permits/export : export CSV en masse (Business et
|
|
472
|
+
"""GET /v1/permits/export : export CSV en masse (Business et Entreprise).
|
|
439
473
|
|
|
440
|
-
Formules Business et
|
|
441
|
-
Cap par defaut : 100k lignes Business, 1M
|
|
474
|
+
Formules Business et Entreprise uniquement (403 sinon).
|
|
475
|
+
Cap par defaut : 100k lignes Business, 1M Entreprise. Le user
|
|
442
476
|
peut reduire via `max_rows` mais pas augmenter.
|
|
443
477
|
|
|
444
478
|
Retourne le contenu CSV brut en bytes UTF-8 (header + rows). Pour
|
|
@@ -611,8 +645,8 @@ class PermisAPI:
|
|
|
611
645
|
Utilise pg_trgm cote DB. Insensible aux accents et a la casse,
|
|
612
646
|
tolerant aux typos courants. Latence ~50-200ms.
|
|
613
647
|
|
|
614
|
-
Toutes formules (Gratuit,
|
|
615
|
-
|
|
648
|
+
Toutes formules (Gratuit, Découverte, Explorateur, Régional, Pro,
|
|
649
|
+
Business, Entreprise), dans le périmètre géographique de la formule. Coût
|
|
616
650
|
quota : 1 unité par résultat retourné (minimum 1).
|
|
617
651
|
|
|
618
652
|
Args:
|
|
@@ -694,8 +728,8 @@ class _AsyncPermitsResource:
|
|
|
694
728
|
return
|
|
695
729
|
page += 1
|
|
696
730
|
|
|
697
|
-
async def get(self, num_pa: str) -> dict[str, Any]:
|
|
698
|
-
return await self._c._get(f"/v1/permits/{num_pa}")
|
|
731
|
+
async def get(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
732
|
+
return await self._c._get(f"/v1/permits/{num_pa}", params=_params_type(type))
|
|
699
733
|
|
|
700
734
|
async def near(
|
|
701
735
|
self, *, lat: float, lng: float, radius_m: int, **filters: Any
|
|
@@ -710,6 +744,7 @@ class _AsyncPermitsResource:
|
|
|
710
744
|
self,
|
|
711
745
|
num_pa: str,
|
|
712
746
|
*,
|
|
747
|
+
type: str | None = None,
|
|
713
748
|
limit: int | None = None,
|
|
714
749
|
type_local: str | None = None,
|
|
715
750
|
include_terrains: bool | None = None,
|
|
@@ -732,33 +767,36 @@ class _AsyncPermitsResource:
|
|
|
732
767
|
params["min_year"] = min_year
|
|
733
768
|
if max_distance is not None:
|
|
734
769
|
params["max_distance"] = max_distance
|
|
735
|
-
return await self._c._get(
|
|
770
|
+
return await self._c._get(
|
|
771
|
+
f"/v1/permits/{num_pa}/dvf",
|
|
772
|
+
params={**params, **(_params_type(type) or {})} or None,
|
|
773
|
+
)
|
|
736
774
|
|
|
737
|
-
async def score_mdb(self, num_pa: str) -> dict[str, Any]:
|
|
775
|
+
async def score_mdb(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
738
776
|
"""GET /v1/permits/{num_pa}/score : version async. Régional et plus."""
|
|
739
|
-
return await self._c._get(f"/v1/permits/{num_pa}/score")
|
|
777
|
+
return await self._c._get(f"/v1/permits/{num_pa}/score", params=_params_type(type))
|
|
740
778
|
|
|
741
|
-
async def plu(self, num_pa: str) -> dict[str, Any]:
|
|
779
|
+
async def plu(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
742
780
|
"""GET /v1/permits/{num_pa}/plu : version async. Régional et plus.
|
|
743
781
|
Voir _PermitsResource.plu pour la doc complete."""
|
|
744
|
-
return await self._c._get(f"/v1/permits/{num_pa}/plu")
|
|
782
|
+
return await self._c._get(f"/v1/permits/{num_pa}/plu", params=_params_type(type))
|
|
745
783
|
|
|
746
|
-
async def risks(self, num_pa: str) -> dict[str, Any]:
|
|
784
|
+
async def risks(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
747
785
|
"""GET /v1/permits/{num_pa}/risks : version async. Régional et plus.
|
|
748
786
|
Voir _PermitsResource.risks pour la doc complete."""
|
|
749
|
-
return await self._c._get(f"/v1/permits/{num_pa}/risks")
|
|
787
|
+
return await self._c._get(f"/v1/permits/{num_pa}/risks", params=_params_type(type))
|
|
750
788
|
|
|
751
|
-
async def parcelle(self, num_pa: str) -> dict[str, Any]:
|
|
789
|
+
async def parcelle(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
752
790
|
"""GET /v1/permits/{num_pa}/parcelle : version async. Régional et plus.
|
|
753
791
|
Voir _PermitsResource.parcelle pour la doc complete."""
|
|
754
|
-
return await self._c._get(f"/v1/permits/{num_pa}/parcelle")
|
|
792
|
+
return await self._c._get(f"/v1/permits/{num_pa}/parcelle", params=_params_type(type))
|
|
755
793
|
|
|
756
|
-
async def batiments_existants(self, num_pa: str) -> dict[str, Any]:
|
|
794
|
+
async def batiments_existants(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
757
795
|
"""GET /v1/permits/{num_pa}/batiments-existants : version async.
|
|
758
796
|
Régional et plus. Voir _PermitsResource.batiments_existants pour la
|
|
759
797
|
doc complete. Use case marchand de biens : terrain nu vs deja bati."""
|
|
760
798
|
return await self._c._get(
|
|
761
|
-
f"/v1/permits/{num_pa}/batiments-existants"
|
|
799
|
+
f"/v1/permits/{num_pa}/batiments-existants", params=_params_type(type)
|
|
762
800
|
)
|
|
763
801
|
|
|
764
802
|
async def bulk_enrich(
|
|
@@ -768,7 +806,7 @@ class _AsyncPermitsResource:
|
|
|
768
806
|
radius_m: int = 500,
|
|
769
807
|
max_matches: int = 3,
|
|
770
808
|
) -> dict[str, Any]:
|
|
771
|
-
"""POST /v1/permits/bulk-enrich : version async. Business et
|
|
809
|
+
"""POST /v1/permits/bulk-enrich : version async. Business et Entreprise.
|
|
772
810
|
Voir _PermitsResource.bulk_enrich pour la doc complete."""
|
|
773
811
|
return await self._c._post(
|
|
774
812
|
"/v1/permits/bulk-enrich",
|
|
@@ -779,14 +817,14 @@ class _AsyncPermitsResource:
|
|
|
779
817
|
},
|
|
780
818
|
)
|
|
781
819
|
|
|
782
|
-
async def full_view(self, num_pa: str) -> dict[str, Any]:
|
|
820
|
+
async def full_view(self, num_pa: str, type: str | None = None) -> dict[str, Any]:
|
|
783
821
|
"""GET /v1/permits/{num_pa}/360 : version async. Vue agregee 6-en-1.
|
|
784
822
|
|
|
785
|
-
Coût quota : 6 unités de Régional à
|
|
823
|
+
Coût quota : 6 unités de Régional à Entreprise, 1 unité pour Gratuit, Découverte et Explorateur.
|
|
786
824
|
Le header `X-RateLimit-Cost` confirme le cout. Voir
|
|
787
825
|
_PermitsResource.full_view pour la doc complete.
|
|
788
826
|
"""
|
|
789
|
-
return await self._c._get(f"/v1/permits/{num_pa}/360")
|
|
827
|
+
return await self._c._get(f"/v1/permits/{num_pa}/360", params=_params_type(type))
|
|
790
828
|
|
|
791
829
|
async def score_feedback(
|
|
792
830
|
self,
|
|
@@ -824,7 +862,7 @@ class _AsyncPermitsResource:
|
|
|
824
862
|
etat_pa: int | None = None,
|
|
825
863
|
max_rows: int | None = None,
|
|
826
864
|
) -> bytes:
|
|
827
|
-
"""GET /v1/permits/export : version async. Business et
|
|
865
|
+
"""GET /v1/permits/export : version async. Business et Entreprise.
|
|
828
866
|
Voir _PermitsResource.export pour la doc complete des params."""
|
|
829
867
|
params: dict[str, Any] = {}
|
|
830
868
|
if dep_code is not None:
|
|
@@ -27,7 +27,7 @@ class PermisAPIUnauthorized(PermisAPIError):
|
|
|
27
27
|
|
|
28
28
|
class PermisAPIForbidden(PermisAPIError):
|
|
29
29
|
"""403 : accès refusé (compte désactivé, département ou historique
|
|
30
|
-
hors de votre formule, export réservé à Business et
|
|
30
|
+
hors de votre formule, export réservé à Business et Entreprise)."""
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class PermisAPIRateLimited(PermisAPIError):
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "permisapi-client"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "Client Python officiel pour PermisAPI : permis de construire de France en API REST."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -520,3 +520,34 @@ def test_custom_http_client() -> None:
|
|
|
520
520
|
client = PermisAPI(api_key="pk_test_xyz", client=h)
|
|
521
521
|
assert client._http is h
|
|
522
522
|
client.close()
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# Argument facultatif `type` (05/11/2026) : un même numéro peut porter
|
|
526
|
+
# plusieurs autorisations (un PC et un PD, par exemple).
|
|
527
|
+
@respx.mock
|
|
528
|
+
def test_type_passe_en_parametre() -> None:
|
|
529
|
+
route = respx.get(f"{BASE}/v1/permits/0330632600176").respond(json={"num_pa": "0330632600176"})
|
|
530
|
+
dvf = respx.get(f"{BASE}/v1/permits/0330632600176/dvf").respond(json={"matches": []})
|
|
531
|
+
with PermisAPI(api_key="pk_test_abc") as client:
|
|
532
|
+
client.permits.get("0330632600176", type="pd")
|
|
533
|
+
client.permits.dvf("0330632600176", type="PD", limit=2)
|
|
534
|
+
client.permits.get("0330632600176")
|
|
535
|
+
assert route.calls[0].request.url.params["type"] == "PD"
|
|
536
|
+
assert "type" not in route.calls[1].request.url.params
|
|
537
|
+
assert dvf.calls.last.request.url.params["type"] == "PD"
|
|
538
|
+
assert dvf.calls.last.request.url.params["limit"] == "2"
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def test_type_inconnu_refuse() -> None:
|
|
542
|
+
with PermisAPI(api_key="pk_test_abc") as client:
|
|
543
|
+
with pytest.raises(ValueError):
|
|
544
|
+
client.permits.get("0330632600176", type="CU")
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@respx.mock
|
|
548
|
+
@pytest.mark.asyncio
|
|
549
|
+
async def test_type_async() -> None:
|
|
550
|
+
route = respx.get(f"{BASE}/v1/permits/0330632600176/360").respond(json={"num_pa": "0330632600176"})
|
|
551
|
+
async with AsyncPermisAPI(api_key="pk_test_abc") as client:
|
|
552
|
+
await client.permits.full_view("0330632600176", type="PA")
|
|
553
|
+
assert route.calls.last.request.url.params["type"] == "PA"
|
|
File without changes
|