permisapi-client 0.8.0__tar.gz → 0.8.1__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.
@@ -36,6 +36,8 @@ SECRETS_INVENTORY.md
36
36
  data/raw/*.csv
37
37
  data/raw/*.zip
38
38
  data/processed/*.parquet
39
+ data/cerema_dvfplus/
40
+ data/cadastre_geojson/
39
41
 
40
42
  # Database
41
43
  *.sqlite
@@ -66,3 +68,7 @@ scrape-downloads/
66
68
 
67
69
  # Local handoff bundle (assembled then zipped, not committed)
68
70
  handoff-bundle/
71
+
72
+ # Exports prospects locaux (donnees derivees, regenerables via scripts/export_prospects.py)
73
+ prospects_export.csv
74
+ prospects_*.csv
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.5
2
+ Name: permisapi-client
3
+ Version: 0.8.1
4
+ Summary: Client Python officiel pour PermisAPI : permis de construire de France en API REST.
5
+ Project-URL: Homepage, https://permisapi.fr
6
+ Project-URL: Documentation, https://permisapi.fr/docs
7
+ Project-URL: Changelog, https://permisapi.fr/changelog
8
+ Author-email: Evan Caroux <evan@permisapi.fr>
9
+ License: MIT
10
+ Keywords: api,construire,france,geocoding,permis,proptech,real-estate,sitadel
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Natural Language :: French
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: httpx>=0.24.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
25
+ Requires-Dist: pytest>=7.4; extra == 'dev'
26
+ Requires-Dist: respx>=0.20; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # permisapi-client
30
+
31
+ Client Python officiel de [PermisAPI](https://permisapi.fr) : 1,3 million de permis de
32
+ construire en France (base Sitadel, permis autorisés depuis 2013, 101 départements) dans
33
+ une API REST.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install permisapi-client
39
+ ```
40
+
41
+ ## Démarrage rapide
42
+
43
+ ```python
44
+ from permisapi_client import PermisAPI
45
+
46
+ client = PermisAPI(api_key="pk_live_...") # clé obtenue sur https://permisapi.fr/#pricing
47
+
48
+ # Liste paginée : {"data": [...], "pagination": {page, limit, total, has_next, next_url}}
49
+ page = client.permits.list(dep_code="75", limit=10)
50
+ for permit in page["data"]:
51
+ print(permit["num_pa"], permit["full_address"])
52
+ print(page["pagination"]["total"], "permis au total")
53
+
54
+ # Recherche géographique (PostGIS) : liste de permis, chacun avec distance_m
55
+ nearby = client.permits.near(lat=48.8599, lng=2.3423, radius_m=300)
56
+
57
+ # Détail d'un permis
58
+ permit = client.permits.get("07510125V0025")
59
+
60
+ # Statistiques d'une commune (Paris entier est rangé sous le code 75056 dans Sitadel)
61
+ stats = client.stats.commune("75056")
62
+ print(stats["total_permits"], stats["granted"], stats["trend_yoy_pct"])
63
+
64
+ # Toutes les pages, jusqu'à pagination.has_next == False
65
+ for p in client.permits.iter_all(dep_code="75", permit_type="PC_LOGEMENT"):
66
+ print(p["num_pa"])
67
+ ```
68
+
69
+ Coût : une recherche (`list`, `iter_all`, `near`, `search`) décompte 1 requête par permis
70
+ 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).
72
+
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
76
+ exemples de cette page portent sur Paris (75).
77
+
78
+ ## Données avancées (formules Régional, Pro, Business et Enterprise)
79
+
80
+ Les formules Gratuit et Explorer reçoivent une erreur 402 sur ces routes.
81
+
82
+ ```python
83
+ num_pa = "07510125V0025"
84
+
85
+ # Ventes immobilières DVF voisines (ventes de 2021 à 2025)
86
+ dvf = client.permits.dvf(num_pa, limit=5)
87
+ for m in dvf["matches"]:
88
+ print(m["date_mutation"], m["valeur_fonciere"], "EUR à", m["distance_m"], "m")
89
+ print("Prix médian au m² :", dvf["dvf_median_price_per_m2"])
90
+
91
+ # Score d'opportunité marchand de biens v0.3 (note 0-100 et palier)
92
+ score = client.permits.score_mdb(num_pa)
93
+ 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.
96
+
97
+ # Zonage PLU (constructible, à urbaniser, agricole, naturel)
98
+ plu = client.permits.plu(num_pa)
99
+ if plu["has_plu"]:
100
+ z = plu["zonage"]
101
+ print(f"Zone {z['code']}, constructible : {z['constructible']}")
102
+
103
+ # Risques Géorisques (inondation, séisme, sites Seveso proches...)
104
+ 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']})")
108
+
109
+ # Votre avis sur un score
110
+ client.permits.score_feedback(
111
+ num_pa=num_pa,
112
+ score_received=72,
113
+ tier_received="high",
114
+ useful=True,
115
+ accuracy="correct",
116
+ comment="Score cohérent avec notre analyse du quartier.",
117
+ )
118
+ ```
119
+
120
+ ## Export CSV (Business et Enterprise)
121
+
122
+ ```python
123
+ # Plafond : 100 000 lignes en Business, 1 000 000 en Enterprise
124
+ csv_bytes = client.permits.export(
125
+ dep_code="75",
126
+ permit_type="PC_LOGEMENT",
127
+ date_from="2024-01-01",
128
+ max_rows=5000,
129
+ )
130
+ with open("paris.csv", "wb") as f:
131
+ f.write(csv_bytes)
132
+ ```
133
+
134
+ ## Serveur MCP (Claude, ChatGPT, Cursor)
135
+
136
+ Le serveur MCP officiel (paquet séparé `permisapi-mcp`) permet à Claude, ChatGPT, Claude
137
+ Desktop, Cursor et Windsurf d'interroger PermisAPI en langage naturel :
138
+
139
+ ```bash
140
+ pip install permisapi-mcp
141
+ ```
142
+
143
+ Configurez votre clé dans le client MCP, puis demandez directement :
144
+ > *« Liste les permis de logement autorisés à Paris ce mois-ci »*
145
+
146
+ Mise en place complète : [permisapi.fr/mcp](https://permisapi.fr/mcp).
147
+
148
+ ## Async
149
+
150
+ ```python
151
+ import asyncio
152
+ from permisapi_client import AsyncPermisAPI
153
+
154
+ async def main():
155
+ async with AsyncPermisAPI(api_key="pk_live_...") as client:
156
+ page = await client.permits.list(dep_code="75")
157
+ print(page["pagination"]["total"])
158
+ async for p in client.permits.iter_all(dep_code="75", permit_type="PC_LOGEMENT"):
159
+ print(p["num_pa"])
160
+
161
+ asyncio.run(main())
162
+ ```
163
+
164
+ ## Alertes par webhook
165
+
166
+ ```python
167
+ alert = client.alerts.create(
168
+ name="Paris 18e rénovations",
169
+ center_lat=48.88,
170
+ center_lng=2.35,
171
+ radius_km=3,
172
+ permit_types=["PC_LOGEMENT"],
173
+ webhook_url="https://votre-application.fr/permisapi-hook",
174
+ )
175
+ # Le webhook_secret n'est renvoyé qu'ICI : conservez-le pour vérifier les signatures HMAC.
176
+ print(alert["webhook_secret"])
177
+ ```
178
+
179
+ ## Gestion des erreurs
180
+
181
+ ```python
182
+ from permisapi_client import (
183
+ PermisAPI,
184
+ PermisAPIRateLimited,
185
+ PermisAPIQuotaExceeded,
186
+ PermisAPIUnauthorized,
187
+ )
188
+
189
+ try:
190
+ client.permits.list(dep_code="75")
191
+ except PermisAPIUnauthorized:
192
+ print("Clé API invalide")
193
+ except PermisAPIQuotaExceeded:
194
+ print("Quota mensuel atteint : il se renouvelle le 1er du mois (UTC)")
195
+ except PermisAPIRateLimited as e:
196
+ print(f"Trop de requêtes par minute, nouvel essai possible dans {e.retry_after} s")
197
+ ```
198
+
199
+ ## Licence
200
+
201
+ MIT. Source des permis : SDES, base Sitadel, data.gouv.fr (Licence Ouverte Etalab).
202
+
203
+ ## Liens
204
+
205
+ - Documentation : https://permisapi.fr/docs
206
+ - Référence complète de l'API : https://api.permisapi.fr/docs
207
+ - Nouveautés : https://permisapi.fr/changelog
@@ -0,0 +1,179 @@
1
+ # permisapi-client
2
+
3
+ Client Python officiel de [PermisAPI](https://permisapi.fr) : 1,3 million de permis de
4
+ construire en France (base Sitadel, permis autorisés depuis 2013, 101 départements) dans
5
+ une API REST.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install permisapi-client
11
+ ```
12
+
13
+ ## Démarrage rapide
14
+
15
+ ```python
16
+ from permisapi_client import PermisAPI
17
+
18
+ client = PermisAPI(api_key="pk_live_...") # clé obtenue sur https://permisapi.fr/#pricing
19
+
20
+ # Liste paginée : {"data": [...], "pagination": {page, limit, total, has_next, next_url}}
21
+ page = client.permits.list(dep_code="75", limit=10)
22
+ for permit in page["data"]:
23
+ print(permit["num_pa"], permit["full_address"])
24
+ print(page["pagination"]["total"], "permis au total")
25
+
26
+ # Recherche géographique (PostGIS) : liste de permis, chacun avec distance_m
27
+ nearby = client.permits.near(lat=48.8599, lng=2.3423, radius_m=300)
28
+
29
+ # Détail d'un permis
30
+ permit = client.permits.get("07510125V0025")
31
+
32
+ # Statistiques d'une commune (Paris entier est rangé sous le code 75056 dans Sitadel)
33
+ stats = client.stats.commune("75056")
34
+ print(stats["total_permits"], stats["granted"], stats["trend_yoy_pct"])
35
+
36
+ # Toutes les pages, jusqu'à pagination.has_next == False
37
+ for p in client.permits.iter_all(dep_code="75", permit_type="PC_LOGEMENT"):
38
+ print(p["num_pa"])
39
+ ```
40
+
41
+ Coût : une recherche (`list`, `iter_all`, `near`, `search`) décompte 1 requête par permis
42
+ 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).
44
+
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
48
+ exemples de cette page portent sur Paris (75).
49
+
50
+ ## Données avancées (formules Régional, Pro, Business et Enterprise)
51
+
52
+ Les formules Gratuit et Explorer reçoivent une erreur 402 sur ces routes.
53
+
54
+ ```python
55
+ num_pa = "07510125V0025"
56
+
57
+ # Ventes immobilières DVF voisines (ventes de 2021 à 2025)
58
+ dvf = client.permits.dvf(num_pa, limit=5)
59
+ for m in dvf["matches"]:
60
+ print(m["date_mutation"], m["valeur_fonciere"], "EUR à", m["distance_m"], "m")
61
+ print("Prix médian au m² :", dvf["dvf_median_price_per_m2"])
62
+
63
+ # Score d'opportunité marchand de biens v0.3 (note 0-100 et palier)
64
+ score = client.permits.score_mdb(num_pa)
65
+ 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.
68
+
69
+ # Zonage PLU (constructible, à urbaniser, agricole, naturel)
70
+ plu = client.permits.plu(num_pa)
71
+ if plu["has_plu"]:
72
+ z = plu["zonage"]
73
+ print(f"Zone {z['code']}, constructible : {z['constructible']}")
74
+
75
+ # Risques Géorisques (inondation, séisme, sites Seveso proches...)
76
+ 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']})")
80
+
81
+ # Votre avis sur un score
82
+ client.permits.score_feedback(
83
+ num_pa=num_pa,
84
+ score_received=72,
85
+ tier_received="high",
86
+ useful=True,
87
+ accuracy="correct",
88
+ comment="Score cohérent avec notre analyse du quartier.",
89
+ )
90
+ ```
91
+
92
+ ## Export CSV (Business et Enterprise)
93
+
94
+ ```python
95
+ # Plafond : 100 000 lignes en Business, 1 000 000 en Enterprise
96
+ csv_bytes = client.permits.export(
97
+ dep_code="75",
98
+ permit_type="PC_LOGEMENT",
99
+ date_from="2024-01-01",
100
+ max_rows=5000,
101
+ )
102
+ with open("paris.csv", "wb") as f:
103
+ f.write(csv_bytes)
104
+ ```
105
+
106
+ ## Serveur MCP (Claude, ChatGPT, Cursor)
107
+
108
+ Le serveur MCP officiel (paquet séparé `permisapi-mcp`) permet à Claude, ChatGPT, Claude
109
+ Desktop, Cursor et Windsurf d'interroger PermisAPI en langage naturel :
110
+
111
+ ```bash
112
+ pip install permisapi-mcp
113
+ ```
114
+
115
+ Configurez votre clé dans le client MCP, puis demandez directement :
116
+ > *« Liste les permis de logement autorisés à Paris ce mois-ci »*
117
+
118
+ Mise en place complète : [permisapi.fr/mcp](https://permisapi.fr/mcp).
119
+
120
+ ## Async
121
+
122
+ ```python
123
+ import asyncio
124
+ from permisapi_client import AsyncPermisAPI
125
+
126
+ async def main():
127
+ async with AsyncPermisAPI(api_key="pk_live_...") as client:
128
+ page = await client.permits.list(dep_code="75")
129
+ print(page["pagination"]["total"])
130
+ async for p in client.permits.iter_all(dep_code="75", permit_type="PC_LOGEMENT"):
131
+ print(p["num_pa"])
132
+
133
+ asyncio.run(main())
134
+ ```
135
+
136
+ ## Alertes par webhook
137
+
138
+ ```python
139
+ alert = client.alerts.create(
140
+ name="Paris 18e rénovations",
141
+ center_lat=48.88,
142
+ center_lng=2.35,
143
+ radius_km=3,
144
+ permit_types=["PC_LOGEMENT"],
145
+ webhook_url="https://votre-application.fr/permisapi-hook",
146
+ )
147
+ # Le webhook_secret n'est renvoyé qu'ICI : conservez-le pour vérifier les signatures HMAC.
148
+ print(alert["webhook_secret"])
149
+ ```
150
+
151
+ ## Gestion des erreurs
152
+
153
+ ```python
154
+ from permisapi_client import (
155
+ PermisAPI,
156
+ PermisAPIRateLimited,
157
+ PermisAPIQuotaExceeded,
158
+ PermisAPIUnauthorized,
159
+ )
160
+
161
+ try:
162
+ client.permits.list(dep_code="75")
163
+ except PermisAPIUnauthorized:
164
+ print("Clé API invalide")
165
+ except PermisAPIQuotaExceeded:
166
+ print("Quota mensuel atteint : il se renouvelle le 1er du mois (UTC)")
167
+ except PermisAPIRateLimited as e:
168
+ print(f"Trop de requêtes par minute, nouvel essai possible dans {e.retry_after} s")
169
+ ```
170
+
171
+ ## Licence
172
+
173
+ MIT. Source des permis : SDES, base Sitadel, data.gouv.fr (Licence Ouverte Etalab).
174
+
175
+ ## Liens
176
+
177
+ - Documentation : https://permisapi.fr/docs
178
+ - Référence complète de l'API : https://api.permisapi.fr/docs
179
+ - Nouveautés : https://permisapi.fr/changelog
@@ -3,8 +3,9 @@
3
3
  Usage sync :
4
4
  from permisapi_client import PermisAPI
5
5
  client = PermisAPI(api_key="pk_live_...")
6
- for permit in client.permits.list(dep_code="75", limit=10):
7
- print(permit["num_pa"], permit["adr_localite_ter"])
6
+ page = client.permits.list(dep_code="75", limit=10)
7
+ for permit in page["data"]:
8
+ print(permit["num_pa"], permit["full_address"])
8
9
 
9
10
  Usage async :
10
11
  from permisapi_client import AsyncPermisAPI
@@ -30,4 +31,4 @@ __all__ = [
30
31
  "PermisAPIValidationError",
31
32
  ]
32
33
 
33
- __version__ = "0.8.0"
34
+ __version__ = "0.8.1"