permisapi-client 0.8.0__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.
@@ -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,220 @@
1
+ Metadata-Version: 2.5
2
+ Name: permisapi-client
3
+ Version: 0.9.0
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
+ # 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
+
67
+ # Statistiques d'une commune (Paris entier est rangé sous le code 75056 dans Sitadel)
68
+ stats = client.stats.commune("75056")
69
+ print(stats["total_permits"], stats["granted"], stats["trend_yoy_pct"])
70
+
71
+ # Toutes les pages, jusqu'à pagination.has_next == False
72
+ for p in client.permits.iter_all(dep_code="75", permit_type="PC_LOGEMENT"):
73
+ print(p["num_pa"])
74
+ ```
75
+
76
+ Coût : une recherche (`list`, `iter_all`, `near`, `search`) décompte 1 requête par permis
77
+ renvoyé. `iter_all` sur un département entier peut donc épuiser le quota mensuel de la
78
+ formule Gratuit (500 requêtes), Découverte (2 000) ou Explorateur (5 000).
79
+
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
84
+ exemples de cette page portent sur Paris (75).
85
+
86
+ ## Données avancées (formules Régional, Pro, Business et Entreprise)
87
+
88
+ Les formules Gratuit, Découverte et Explorateur reçoivent une erreur 402 sur ces routes.
89
+
90
+ ```python
91
+ num_pa = "07510125V0025"
92
+
93
+ # Ventes immobilières DVF voisines (ventes de 2021 à 2025)
94
+ dvf = client.permits.dvf(num_pa, limit=5)
95
+ for m in dvf["matches"]:
96
+ print(m["date_mutation"], m["valeur_fonciere"], "EUR à", m["distance_m"], "m")
97
+ print("Prix médian au m² :", dvf["dvf_median_price_per_m2"])
98
+
99
+ # Score d'opportunité marchand de biens v0.3 (note 0-100 et palier)
100
+ score = client.permits.score_mdb(num_pa)
101
+ print(f"Score : {score['score']}/100 ({score['tier']})")
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.
104
+
105
+ # Zonage PLU (constructible, à urbaniser, agricole, naturel)
106
+ plu = client.permits.plu(num_pa)
107
+ if plu["has_plu"]:
108
+ z = plu["zonage"]
109
+ print(f"Zone {z['code']}, constructible : {z['constructible']}")
110
+
111
+ # Risques Géorisques (inondation, séisme, sites Seveso proches...)
112
+ risks = client.permits.risks(num_pa)
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)
116
+
117
+ # Votre avis sur un score
118
+ client.permits.score_feedback(
119
+ num_pa=num_pa,
120
+ score_received=72,
121
+ tier_received="high",
122
+ useful=True,
123
+ accuracy="correct",
124
+ comment="Score cohérent avec notre analyse du quartier.",
125
+ )
126
+ ```
127
+
128
+ ## Export CSV (Business et Entreprise)
129
+
130
+ ```python
131
+ # Plafond : 100 000 lignes en Business, 1 000 000 en Entreprise
132
+ csv_bytes = client.permits.export(
133
+ dep_code="75",
134
+ permit_type="PC_LOGEMENT",
135
+ date_from="2024-01-01",
136
+ max_rows=5000,
137
+ )
138
+ with open("paris.csv", "wb") as f:
139
+ f.write(csv_bytes)
140
+ ```
141
+
142
+ ## Serveur MCP (Claude, ChatGPT, Cursor)
143
+
144
+ Le serveur MCP officiel (paquet séparé `permisapi-mcp`) permet à Claude, ChatGPT, Claude
145
+ Desktop, Cursor et Windsurf d'interroger PermisAPI en langage naturel :
146
+
147
+ ```bash
148
+ pip install permisapi-mcp
149
+ ```
150
+
151
+ Configurez votre clé dans le client MCP, puis demandez directement :
152
+ > *« Liste les permis de logement autorisés à Paris ce mois-ci »*
153
+
154
+ Mise en place complète : [permisapi.fr/mcp](https://permisapi.fr/mcp).
155
+
156
+ ## Async
157
+
158
+ ```python
159
+ import asyncio
160
+ from permisapi_client import AsyncPermisAPI
161
+
162
+ async def main():
163
+ async with AsyncPermisAPI(api_key="pk_live_...") as client:
164
+ page = await client.permits.list(dep_code="75")
165
+ print(page["pagination"]["total"])
166
+ async for p in client.permits.iter_all(dep_code="75", permit_type="PC_LOGEMENT"):
167
+ print(p["num_pa"])
168
+
169
+ asyncio.run(main())
170
+ ```
171
+
172
+ ## Alertes par webhook
173
+
174
+ ```python
175
+ alert = client.alerts.create(
176
+ name="Paris 18e rénovations",
177
+ center_lat=48.88,
178
+ center_lng=2.35,
179
+ radius_km=3,
180
+ permit_types=["PC_LOGEMENT"],
181
+ webhook_url="https://votre-application.fr/permisapi-hook",
182
+ )
183
+ # Le webhook_secret n'est renvoyé qu'ICI : conservez-le pour vérifier les signatures HMAC.
184
+ print(alert["webhook_secret"])
185
+ ```
186
+
187
+ ## Gestion des erreurs
188
+
189
+ ```python
190
+ from permisapi_client import (
191
+ PermisAPI,
192
+ PermisAPIRateLimited,
193
+ PermisAPIQuotaExceeded,
194
+ PermisAPIUnauthorized,
195
+ )
196
+
197
+ try:
198
+ client.permits.list(dep_code="75")
199
+ except PermisAPIUnauthorized:
200
+ print("Clé API invalide")
201
+ except PermisAPIQuotaExceeded:
202
+ print("Quota mensuel atteint : il se renouvelle le 1er du mois (UTC)")
203
+ except PermisAPIRateLimited as e:
204
+ print(f"Trop de requêtes par minute, nouvel essai possible dans {e.retry_after} s")
205
+ ```
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
+
212
+ ## Licence
213
+
214
+ MIT. Source des permis : SDES, base Sitadel, data.gouv.fr (Licence Ouverte Etalab).
215
+
216
+ ## Liens
217
+
218
+ - Documentation : https://permisapi.fr/docs
219
+ - Référence complète de l'API : https://api.permisapi.fr/docs
220
+ - Nouveautés : https://permisapi.fr/changelog
@@ -0,0 +1,192 @@
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
+ # 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
+
39
+ # Statistiques d'une commune (Paris entier est rangé sous le code 75056 dans Sitadel)
40
+ stats = client.stats.commune("75056")
41
+ print(stats["total_permits"], stats["granted"], stats["trend_yoy_pct"])
42
+
43
+ # Toutes les pages, jusqu'à pagination.has_next == False
44
+ for p in client.permits.iter_all(dep_code="75", permit_type="PC_LOGEMENT"):
45
+ print(p["num_pa"])
46
+ ```
47
+
48
+ Coût : une recherche (`list`, `iter_all`, `near`, `search`) décompte 1 requête par permis
49
+ renvoyé. `iter_all` sur un département entier peut donc épuiser le quota mensuel de la
50
+ formule Gratuit (500 requêtes), Découverte (2 000) ou Explorateur (5 000).
51
+
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
56
+ exemples de cette page portent sur Paris (75).
57
+
58
+ ## Données avancées (formules Régional, Pro, Business et Entreprise)
59
+
60
+ Les formules Gratuit, Découverte et Explorateur reçoivent une erreur 402 sur ces routes.
61
+
62
+ ```python
63
+ num_pa = "07510125V0025"
64
+
65
+ # Ventes immobilières DVF voisines (ventes de 2021 à 2025)
66
+ dvf = client.permits.dvf(num_pa, limit=5)
67
+ for m in dvf["matches"]:
68
+ print(m["date_mutation"], m["valeur_fonciere"], "EUR à", m["distance_m"], "m")
69
+ print("Prix médian au m² :", dvf["dvf_median_price_per_m2"])
70
+
71
+ # Score d'opportunité marchand de biens v0.3 (note 0-100 et palier)
72
+ score = client.permits.score_mdb(num_pa)
73
+ print(f"Score : {score['score']}/100 ({score['tier']})")
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.
76
+
77
+ # Zonage PLU (constructible, à urbaniser, agricole, naturel)
78
+ plu = client.permits.plu(num_pa)
79
+ if plu["has_plu"]:
80
+ z = plu["zonage"]
81
+ print(f"Zone {z['code']}, constructible : {z['constructible']}")
82
+
83
+ # Risques Géorisques (inondation, séisme, sites Seveso proches...)
84
+ risks = client.permits.risks(num_pa)
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)
88
+
89
+ # Votre avis sur un score
90
+ client.permits.score_feedback(
91
+ num_pa=num_pa,
92
+ score_received=72,
93
+ tier_received="high",
94
+ useful=True,
95
+ accuracy="correct",
96
+ comment="Score cohérent avec notre analyse du quartier.",
97
+ )
98
+ ```
99
+
100
+ ## Export CSV (Business et Entreprise)
101
+
102
+ ```python
103
+ # Plafond : 100 000 lignes en Business, 1 000 000 en Entreprise
104
+ csv_bytes = client.permits.export(
105
+ dep_code="75",
106
+ permit_type="PC_LOGEMENT",
107
+ date_from="2024-01-01",
108
+ max_rows=5000,
109
+ )
110
+ with open("paris.csv", "wb") as f:
111
+ f.write(csv_bytes)
112
+ ```
113
+
114
+ ## Serveur MCP (Claude, ChatGPT, Cursor)
115
+
116
+ Le serveur MCP officiel (paquet séparé `permisapi-mcp`) permet à Claude, ChatGPT, Claude
117
+ Desktop, Cursor et Windsurf d'interroger PermisAPI en langage naturel :
118
+
119
+ ```bash
120
+ pip install permisapi-mcp
121
+ ```
122
+
123
+ Configurez votre clé dans le client MCP, puis demandez directement :
124
+ > *« Liste les permis de logement autorisés à Paris ce mois-ci »*
125
+
126
+ Mise en place complète : [permisapi.fr/mcp](https://permisapi.fr/mcp).
127
+
128
+ ## Async
129
+
130
+ ```python
131
+ import asyncio
132
+ from permisapi_client import AsyncPermisAPI
133
+
134
+ async def main():
135
+ async with AsyncPermisAPI(api_key="pk_live_...") as client:
136
+ page = await client.permits.list(dep_code="75")
137
+ print(page["pagination"]["total"])
138
+ async for p in client.permits.iter_all(dep_code="75", permit_type="PC_LOGEMENT"):
139
+ print(p["num_pa"])
140
+
141
+ asyncio.run(main())
142
+ ```
143
+
144
+ ## Alertes par webhook
145
+
146
+ ```python
147
+ alert = client.alerts.create(
148
+ name="Paris 18e rénovations",
149
+ center_lat=48.88,
150
+ center_lng=2.35,
151
+ radius_km=3,
152
+ permit_types=["PC_LOGEMENT"],
153
+ webhook_url="https://votre-application.fr/permisapi-hook",
154
+ )
155
+ # Le webhook_secret n'est renvoyé qu'ICI : conservez-le pour vérifier les signatures HMAC.
156
+ print(alert["webhook_secret"])
157
+ ```
158
+
159
+ ## Gestion des erreurs
160
+
161
+ ```python
162
+ from permisapi_client import (
163
+ PermisAPI,
164
+ PermisAPIRateLimited,
165
+ PermisAPIQuotaExceeded,
166
+ PermisAPIUnauthorized,
167
+ )
168
+
169
+ try:
170
+ client.permits.list(dep_code="75")
171
+ except PermisAPIUnauthorized:
172
+ print("Clé API invalide")
173
+ except PermisAPIQuotaExceeded:
174
+ print("Quota mensuel atteint : il se renouvelle le 1er du mois (UTC)")
175
+ except PermisAPIRateLimited as e:
176
+ print(f"Trop de requêtes par minute, nouvel essai possible dans {e.retry_after} s")
177
+ ```
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
+
184
+ ## Licence
185
+
186
+ MIT. Source des permis : SDES, base Sitadel, data.gouv.fr (Licence Ouverte Etalab).
187
+
188
+ ## Liens
189
+
190
+ - Documentation : https://permisapi.fr/docs
191
+ - Référence complète de l'API : https://api.permisapi.fr/docs
192
+ - 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.9.0"