permisapi-client 0.4.0__tar.gz → 0.6.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.4.0 → permisapi_client-0.6.0}/PKG-INFO +65 -1
- {permisapi_client-0.4.0 → permisapi_client-0.6.0}/README.md +64 -0
- {permisapi_client-0.4.0 → permisapi_client-0.6.0}/permisapi_client/__init__.py +1 -1
- {permisapi_client-0.4.0 → permisapi_client-0.6.0}/permisapi_client/client.py +111 -3
- {permisapi_client-0.4.0 → permisapi_client-0.6.0}/pyproject.toml +1 -1
- {permisapi_client-0.4.0 → permisapi_client-0.6.0}/.gitignore +0 -0
- {permisapi_client-0.4.0 → permisapi_client-0.6.0}/permisapi_client/exceptions.py +0 -0
- {permisapi_client-0.4.0 → permisapi_client-0.6.0}/tests/test_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: permisapi-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -63,6 +63,70 @@ for p in client.permits.iter_all(dep_code="75"):
|
|
|
63
63
|
print(p["num_pa"])
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## Cross-ref Pro+ (DVF, Score MDB, PLU, Géorisques)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
# Top 5 transactions DVF voisines (5 ans glissants)
|
|
70
|
+
dvf = client.permits.dvf("PC07404021K1", limit=5)
|
|
71
|
+
for m in dvf["matches"]:
|
|
72
|
+
print(m["valeur_fonciere"], "EUR a", m["distance_m"], "m")
|
|
73
|
+
|
|
74
|
+
# Score Opportunite Marchand de Biens v0.1 (note 0-100 + tier)
|
|
75
|
+
score = client.permits.score_mdb("PC07404021K1")
|
|
76
|
+
print(f"Score: {score['score']}/100 ({score['tier']})")
|
|
77
|
+
|
|
78
|
+
# Zonage urbanisme PLU (constructible vs agricole vs naturelle)
|
|
79
|
+
plu = client.permits.plu("PC07404021K1")
|
|
80
|
+
if plu["has_plu"]:
|
|
81
|
+
z = plu["zonage"]
|
|
82
|
+
print(f"Zone {z['code']} - constructible: {z['constructible']}")
|
|
83
|
+
|
|
84
|
+
# Georisques BRGM (inondation, seisme, argile, ICPE proches)
|
|
85
|
+
risks = client.permits.risks("PC07404021K1")
|
|
86
|
+
print(f"Risk score: {risks['risk_score']}/100 ({risks['risk_tier']})")
|
|
87
|
+
for risk in risks["risks"]:
|
|
88
|
+
print(f"- {risk['label']} (PPR: {risk['has_ppr']})")
|
|
89
|
+
|
|
90
|
+
# Feedback sur le Score MDB (alimente le training V1.0 ML)
|
|
91
|
+
client.permits.score_feedback(
|
|
92
|
+
num_pa="PC07404021K1",
|
|
93
|
+
score_received=78,
|
|
94
|
+
tier_received="high",
|
|
95
|
+
useful=False,
|
|
96
|
+
accuracy="too_high",
|
|
97
|
+
comment="SCI familiale, pas un MDB. Le signal SCI=90 est trop generique.",
|
|
98
|
+
recommendation="Distinguer SCI familiale via volume de permis.",
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Export bulk CSV (Business+)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# Cap par defaut : 100k Business / 1M Enterprise
|
|
106
|
+
csv_bytes = client.permits.export(
|
|
107
|
+
dep_code="33",
|
|
108
|
+
permit_type="PC_LOGEMENT",
|
|
109
|
+
date_from="2024-01-01",
|
|
110
|
+
max_rows=5000,
|
|
111
|
+
)
|
|
112
|
+
with open("bordeaux.csv", "wb") as f:
|
|
113
|
+
f.write(csv_bytes)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## MCP server (Claude / ChatGPT / Cursor)
|
|
117
|
+
|
|
118
|
+
Le serveur MCP officiel (package separe `permisapi-mcp`) permet a Claude
|
|
119
|
+
Desktop, Cursor et Windsurf d'appeler PermisAPI en langage naturel :
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install permisapi-mcp
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Configure ta cle dans le client MCP, puis demande directement :
|
|
126
|
+
> *"Liste les permis de logement deposes a Bordeaux ce mois avec un score MDB > 70"*
|
|
127
|
+
|
|
128
|
+
Voir [permisapi.fr/mcp](https://permisapi.fr/mcp) pour le setup complet.
|
|
129
|
+
|
|
66
130
|
## Async
|
|
67
131
|
|
|
68
132
|
```python
|
|
@@ -35,6 +35,70 @@ for p in client.permits.iter_all(dep_code="75"):
|
|
|
35
35
|
print(p["num_pa"])
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Cross-ref Pro+ (DVF, Score MDB, PLU, Géorisques)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# Top 5 transactions DVF voisines (5 ans glissants)
|
|
42
|
+
dvf = client.permits.dvf("PC07404021K1", limit=5)
|
|
43
|
+
for m in dvf["matches"]:
|
|
44
|
+
print(m["valeur_fonciere"], "EUR a", m["distance_m"], "m")
|
|
45
|
+
|
|
46
|
+
# Score Opportunite Marchand de Biens v0.1 (note 0-100 + tier)
|
|
47
|
+
score = client.permits.score_mdb("PC07404021K1")
|
|
48
|
+
print(f"Score: {score['score']}/100 ({score['tier']})")
|
|
49
|
+
|
|
50
|
+
# Zonage urbanisme PLU (constructible vs agricole vs naturelle)
|
|
51
|
+
plu = client.permits.plu("PC07404021K1")
|
|
52
|
+
if plu["has_plu"]:
|
|
53
|
+
z = plu["zonage"]
|
|
54
|
+
print(f"Zone {z['code']} - constructible: {z['constructible']}")
|
|
55
|
+
|
|
56
|
+
# Georisques BRGM (inondation, seisme, argile, ICPE proches)
|
|
57
|
+
risks = client.permits.risks("PC07404021K1")
|
|
58
|
+
print(f"Risk score: {risks['risk_score']}/100 ({risks['risk_tier']})")
|
|
59
|
+
for risk in risks["risks"]:
|
|
60
|
+
print(f"- {risk['label']} (PPR: {risk['has_ppr']})")
|
|
61
|
+
|
|
62
|
+
# Feedback sur le Score MDB (alimente le training V1.0 ML)
|
|
63
|
+
client.permits.score_feedback(
|
|
64
|
+
num_pa="PC07404021K1",
|
|
65
|
+
score_received=78,
|
|
66
|
+
tier_received="high",
|
|
67
|
+
useful=False,
|
|
68
|
+
accuracy="too_high",
|
|
69
|
+
comment="SCI familiale, pas un MDB. Le signal SCI=90 est trop generique.",
|
|
70
|
+
recommendation="Distinguer SCI familiale via volume de permis.",
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Export bulk CSV (Business+)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# Cap par defaut : 100k Business / 1M Enterprise
|
|
78
|
+
csv_bytes = client.permits.export(
|
|
79
|
+
dep_code="33",
|
|
80
|
+
permit_type="PC_LOGEMENT",
|
|
81
|
+
date_from="2024-01-01",
|
|
82
|
+
max_rows=5000,
|
|
83
|
+
)
|
|
84
|
+
with open("bordeaux.csv", "wb") as f:
|
|
85
|
+
f.write(csv_bytes)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## MCP server (Claude / ChatGPT / Cursor)
|
|
89
|
+
|
|
90
|
+
Le serveur MCP officiel (package separe `permisapi-mcp`) permet a Claude
|
|
91
|
+
Desktop, Cursor et Windsurf d'appeler PermisAPI en langage naturel :
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install permisapi-mcp
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Configure ta cle dans le client MCP, puis demande directement :
|
|
98
|
+
> *"Liste les permis de logement deposes a Bordeaux ce mois avec un score MDB > 70"*
|
|
99
|
+
|
|
100
|
+
Voir [permisapi.fr/mcp](https://permisapi.fr/mcp) pour le setup complet.
|
|
101
|
+
|
|
38
102
|
## Async
|
|
39
103
|
|
|
40
104
|
```python
|
|
@@ -13,7 +13,7 @@ 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.6.0"
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _raise_for_status(resp: httpx.Response) -> None:
|
|
@@ -138,12 +138,85 @@ class _PermitsResource:
|
|
|
138
138
|
high/premium), components (7 signaux ponderes), weights,
|
|
139
139
|
version, method, dvf_matches_used, density_class}.
|
|
140
140
|
|
|
141
|
-
Algo : heuristique ponderee qui combine type
|
|
141
|
+
Algo : heuristique ponderee qui combine type permis, surface,
|
|
142
142
|
DVF voisin €/m², activite dep, demandeur (SCI vs promoteur),
|
|
143
|
-
coherence DVF/
|
|
143
|
+
coherence DVF/permis, densite INSEE.
|
|
144
144
|
"""
|
|
145
145
|
return self._c._get(f"/v1/permits/{num_pa}/score")
|
|
146
146
|
|
|
147
|
+
def risks(self, num_pa: str) -> dict[str, Any]:
|
|
148
|
+
"""GET /v1/permits/{num_pa}/risks : Georisques BRGM (commune-level).
|
|
149
|
+
|
|
150
|
+
Plans Pro et superieurs uniquement (402 si Free / Explorer). Source :
|
|
151
|
+
API Georisques (ministere Transition Ecologique + BRGM) live, pas de
|
|
152
|
+
cache V0.1, latence 200-1000ms typique.
|
|
153
|
+
|
|
154
|
+
Retourne {num_pa, comm_code, risk_score (0-100), risk_tier (low/
|
|
155
|
+
moderate/high/critical), risks: [{code, label, has_ppr, ppr_type}],
|
|
156
|
+
icpe_proches: [{nom, regime, distance_m, activite}], fetch_errors,
|
|
157
|
+
source, fetched_at, disclaimer}.
|
|
158
|
+
|
|
159
|
+
En cas d'erreur partielle (ex: ICPE OK mais /risques 500), retourne
|
|
160
|
+
ce qui marche + liste les endpoints qui ont fail dans `fetch_errors`.
|
|
161
|
+
"""
|
|
162
|
+
return self._c._get(f"/v1/permits/{num_pa}/risks")
|
|
163
|
+
|
|
164
|
+
def plu(self, num_pa: str) -> dict[str, Any]:
|
|
165
|
+
"""GET /v1/permits/{num_pa}/plu : zonage urbanisme PLU au point geocode.
|
|
166
|
+
|
|
167
|
+
Plans Pro et superieurs uniquement (402 si Free / Explorer). Source :
|
|
168
|
+
Geoportail de l'Urbanisme via apicarto.ign.fr (live API, latence
|
|
169
|
+
200-500ms typique).
|
|
170
|
+
|
|
171
|
+
Retourne {num_pa, lat, lng, has_plu, zonage : {code (UA/UB/AU/A/N..),
|
|
172
|
+
libelle, type_zone, constructible (bool), constructible_reason,
|
|
173
|
+
plu_revision_date, plu_partition}, other_zones, source, fetched_at}.
|
|
174
|
+
|
|
175
|
+
Si la commune n'a pas digitalise son PLU sur Geoportail (~20% des
|
|
176
|
+
communes FR), `has_plu=False` avec `not_covered_reason` explicite.
|
|
177
|
+
"""
|
|
178
|
+
return self._c._get(f"/v1/permits/{num_pa}/plu")
|
|
179
|
+
|
|
180
|
+
def score_feedback(
|
|
181
|
+
self,
|
|
182
|
+
*,
|
|
183
|
+
num_pa: str,
|
|
184
|
+
score_received: int,
|
|
185
|
+
tier_received: str,
|
|
186
|
+
useful: bool,
|
|
187
|
+
accuracy: str,
|
|
188
|
+
comment: str,
|
|
189
|
+
recommendation: str | None = None,
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
"""POST /v1/score/feedback : envoyer un feedback sur un Score MDB.
|
|
192
|
+
|
|
193
|
+
Plans Pro et superieurs uniquement. Le feedback est stocke en DB
|
|
194
|
+
et un email est envoye au founder pour suivi rapide.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
num_pa: identifiant du permis concerne
|
|
198
|
+
score_received: score 0-100 que tu as recu via /score
|
|
199
|
+
tier_received: "low" | "medium" | "high" | "premium"
|
|
200
|
+
useful: True si le score t'a ete utile
|
|
201
|
+
accuracy: "correct" | "too_high" | "too_low" | "wrong_signals"
|
|
202
|
+
comment: ton retour libre (1-2000 chars)
|
|
203
|
+
recommendation: optionnel, ce qui pourrait etre ameliore (1-2000 chars)
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
{id, num_pa, email_sent, message}
|
|
207
|
+
"""
|
|
208
|
+
body: dict[str, Any] = {
|
|
209
|
+
"num_pa": num_pa,
|
|
210
|
+
"score_received": score_received,
|
|
211
|
+
"tier_received": tier_received,
|
|
212
|
+
"useful": useful,
|
|
213
|
+
"accuracy": accuracy,
|
|
214
|
+
"comment": comment,
|
|
215
|
+
}
|
|
216
|
+
if recommendation is not None:
|
|
217
|
+
body["recommendation"] = recommendation
|
|
218
|
+
return self._c._post("/v1/score/feedback", json=body)
|
|
219
|
+
|
|
147
220
|
def export(
|
|
148
221
|
self,
|
|
149
222
|
*,
|
|
@@ -423,6 +496,41 @@ class _AsyncPermitsResource:
|
|
|
423
496
|
"""GET /v1/permits/{num_pa}/score : version async. Plans Pro+ only."""
|
|
424
497
|
return await self._c._get(f"/v1/permits/{num_pa}/score")
|
|
425
498
|
|
|
499
|
+
async def plu(self, num_pa: str) -> dict[str, Any]:
|
|
500
|
+
"""GET /v1/permits/{num_pa}/plu : version async. Plans Pro+ only.
|
|
501
|
+
Voir _PermitsResource.plu pour la doc complete."""
|
|
502
|
+
return await self._c._get(f"/v1/permits/{num_pa}/plu")
|
|
503
|
+
|
|
504
|
+
async def risks(self, num_pa: str) -> dict[str, Any]:
|
|
505
|
+
"""GET /v1/permits/{num_pa}/risks : version async. Plans Pro+ only.
|
|
506
|
+
Voir _PermitsResource.risks pour la doc complete."""
|
|
507
|
+
return await self._c._get(f"/v1/permits/{num_pa}/risks")
|
|
508
|
+
|
|
509
|
+
async def score_feedback(
|
|
510
|
+
self,
|
|
511
|
+
*,
|
|
512
|
+
num_pa: str,
|
|
513
|
+
score_received: int,
|
|
514
|
+
tier_received: str,
|
|
515
|
+
useful: bool,
|
|
516
|
+
accuracy: str,
|
|
517
|
+
comment: str,
|
|
518
|
+
recommendation: str | None = None,
|
|
519
|
+
) -> dict[str, Any]:
|
|
520
|
+
"""POST /v1/score/feedback : version async. Plans Pro+ only.
|
|
521
|
+
Voir _PermitsResource.score_feedback pour la doc complete."""
|
|
522
|
+
body: dict[str, Any] = {
|
|
523
|
+
"num_pa": num_pa,
|
|
524
|
+
"score_received": score_received,
|
|
525
|
+
"tier_received": tier_received,
|
|
526
|
+
"useful": useful,
|
|
527
|
+
"accuracy": accuracy,
|
|
528
|
+
"comment": comment,
|
|
529
|
+
}
|
|
530
|
+
if recommendation is not None:
|
|
531
|
+
body["recommendation"] = recommendation
|
|
532
|
+
return await self._c._post("/v1/score/feedback", json=body)
|
|
533
|
+
|
|
426
534
|
async def export(
|
|
427
535
|
self,
|
|
428
536
|
*,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|