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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: permisapi-client
3
- Version: 0.8.1
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 Explorer (5 000).
78
+ formule Gratuit (500 requêtes), Découverte (2 000) ou Explorateur (5 000).
72
79
 
73
- Formules : **Gratuit** (1 département, 6 mois d'historique), **Explorer** (5 départements,
74
- 6 mois), **Régional** (5 départements, historique complet et données avancées), **Pro**
75
- (les mêmes données avancées sur la France entière), **Business** et **Enterprise**. Les
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 Enterprise)
86
+ ## Données avancées (formules Régional, Pro, Business et Entreprise)
79
87
 
80
- Les formules Gratuit et Explorer reçoivent une erreur 402 sur ces routes.
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
- # Score déjà pré-calculé : components vaut None ; le détail des 11 signaux
95
- # est donné par la route /v1/permits/{num_pa}/score/explain.
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(f"Risque : {risks['risk_score']}/100 ({risks['risk_tier']})")
106
- for risk in risks["risks"]:
107
- print(f"- {risk['label']} (PPR : {risk['has_ppr']})")
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 Enterprise)
128
+ ## Export CSV (Business et Entreprise)
121
129
 
122
130
  ```python
123
- # Plafond : 100 000 lignes en Business, 1 000 000 en Enterprise
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 Explorer (5 000).
50
+ formule Gratuit (500 requêtes), Découverte (2 000) ou Explorateur (5 000).
44
51
 
45
- Formules : **Gratuit** (1 département, 6 mois d'historique), **Explorer** (5 départements,
46
- 6 mois), **Régional** (5 départements, historique complet et données avancées), **Pro**
47
- (les mêmes données avancées sur la France entière), **Business** et **Enterprise**. Les
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 Enterprise)
58
+ ## Données avancées (formules Régional, Pro, Business et Entreprise)
51
59
 
52
- Les formules Gratuit et Explorer reçoivent une erreur 402 sur ces routes.
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
- # Score déjà pré-calculé : components vaut None ; le détail des 11 signaux
67
- # est donné par la route /v1/permits/{num_pa}/score/explain.
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(f"Risque : {risks['risk_score']}/100 ({risks['risk_tier']})")
78
- for risk in risks["risks"]:
79
- print(f"- {risk['label']} (PPR : {risk['has_ppr']})")
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 Enterprise)
100
+ ## Export CSV (Business et Entreprise)
93
101
 
94
102
  ```python
95
- # Plafond : 100 000 lignes en Business, 1 000 000 en Enterprise
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).
@@ -31,4 +31,4 @@ __all__ = [
31
31
  "PermisAPIValidationError",
32
32
  ]
33
33
 
34
- __version__ = "0.8.1"
34
+ __version__ = "0.9.0"
@@ -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.8.1"
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
- Enterprise ; 402 pour Gratuit et Explorer) : retourne uniquement
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 Explorer (5 000).
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 Explorer)."""
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 Enterprise (402 pour Gratuit
184
- et Explorer).
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 Explorer).
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 Enterprise uniquement (402 pour Gratuit,
251
- Explorer, Régional et Pro).
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 Explorer).
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, filters_applied,
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(f"/v1/permits/{num_pa}/dvf", params=params or None)
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 Explorer).
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
- Quand le score est déjà pré-calculé (cas le plus fréquent,
320
- `method="materialise (precomputed)"`), `components` et `weights`
321
- valent `None` : le détail des 11 signaux (type, surface,
322
- dvf_value, dep_activity, demandeur, dvf_coherence, density, plu,
323
- risk, sirene, building_density) est alors donné par
324
- `GET /v1/permits/{num_pa}/score/explain`. En calcul à la demande,
325
- la réponse ajoute `dvf_matches_used` et `density_class`.
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 Explorer). Source :
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), risks: [{code, label, has_ppr, ppr_type}],
337
- icpe_proches: [{nom, regime, distance_m, activite}], fetch_errors,
338
- source, fetched_at, disclaimer}.
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 Explorer). Source :
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 Enterprise ; 1 unité pour Gratuit et Explorer, qui reçoivent
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 Explorer : `plan_required_for_full` est
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 Explorer). Le
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 Enterprise).
472
+ """GET /v1/permits/export : export CSV en masse (Business et Entreprise).
439
473
 
440
- Formules Business et Enterprise uniquement (403 sinon).
441
- Cap par defaut : 100k lignes Business, 1M Enterprise. Le user
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, Explorer, Régional, Pro, Business,
615
- Enterprise), dans le périmètre géographique de la formule. Coût
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(f"/v1/permits/{num_pa}/dvf", params=params or None)
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 Enterprise.
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 à Enterprise, 1 unité pour Gratuit et Explorer.
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 Enterprise.
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 Enterprise)."""
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.8.1"
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"