permisapi-client 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,33 @@
1
+ """Client Python officiel pour PermisAPI.
2
+
3
+ Usage sync :
4
+ from permisapi_client import PermisAPI
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"])
8
+
9
+ Usage async :
10
+ from permisapi_client import AsyncPermisAPI
11
+ async with AsyncPermisAPI(api_key="pk_live_...") as client:
12
+ permits = await client.permits.near(lat=48.85, lng=2.35, radius_m=500)
13
+ """
14
+ from permisapi_client.client import AsyncPermisAPI, PermisAPI
15
+ from permisapi_client.exceptions import (
16
+ PermisAPIError,
17
+ PermisAPIQuotaExceeded,
18
+ PermisAPIRateLimited,
19
+ PermisAPIUnauthorized,
20
+ PermisAPIValidationError,
21
+ )
22
+
23
+ __all__ = [
24
+ "PermisAPI",
25
+ "AsyncPermisAPI",
26
+ "PermisAPIError",
27
+ "PermisAPIUnauthorized",
28
+ "PermisAPIRateLimited",
29
+ "PermisAPIQuotaExceeded",
30
+ "PermisAPIValidationError",
31
+ ]
32
+
33
+ __version__ = "0.1.0"
@@ -0,0 +1,384 @@
1
+ """PermisAPI sync + async client.
2
+
3
+ Minimal, no ORM. Returns dict / list[dict] pour rester flexible aux
4
+ futurs changements de schema cote API.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Iterator, AsyncIterator
9
+
10
+ import httpx
11
+
12
+ from permisapi_client.exceptions import error_from_response
13
+
14
+ DEFAULT_BASE_URL = "https://api.permisapi.fr"
15
+ DEFAULT_TIMEOUT = 15.0
16
+ USER_AGENT = "permisapi-client-python/0.1.0"
17
+
18
+
19
+ def _raise_for_status(resp: httpx.Response) -> None:
20
+ if 200 <= resp.status_code < 300:
21
+ return
22
+ try:
23
+ payload = resp.json()
24
+ except ValueError:
25
+ payload = {"message": resp.text[:200] or "Erreur inconnue"}
26
+ if "retry_after" not in payload and "Retry-After" in resp.headers:
27
+ try:
28
+ payload["retry_after"] = int(resp.headers["Retry-After"])
29
+ except ValueError:
30
+ pass
31
+ raise error_from_response(resp.status_code, payload)
32
+
33
+
34
+ def _build_headers(api_key: str, extra: dict[str, str] | None = None) -> dict[str, str]:
35
+ h = {
36
+ "X-API-Key": api_key,
37
+ "Accept": "application/json",
38
+ "User-Agent": USER_AGENT,
39
+ }
40
+ if extra:
41
+ h.update(extra)
42
+ return h
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Sync client
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ class _PermitsResource:
51
+ def __init__(self, client: PermisAPI) -> None:
52
+ self._c = client
53
+
54
+ def list(self, **params: Any) -> dict[str, Any]:
55
+ """GET /v1/permits avec filtres. Retourne {items, pagination}."""
56
+ return self._c._get("/v1/permits", params=params)
57
+
58
+ def iter_all(self, **params: Any) -> Iterator[dict[str, Any]]:
59
+ """Itere sur toutes les pages (limite fixee par le plan)."""
60
+ page = 1
61
+ per_page = params.get("limit", 100)
62
+ while True:
63
+ p = {**params, "page": page, "limit": per_page}
64
+ data = self.list(**p)
65
+ items = data.get("items", [])
66
+ if not items:
67
+ return
68
+ yield from items
69
+ pages = (data.get("pagination") or {}).get("pages", 1)
70
+ if page >= pages:
71
+ return
72
+ page += 1
73
+
74
+ def get(self, num_pa: str) -> dict[str, Any]:
75
+ """GET /v1/permits/{num_pa}."""
76
+ return self._c._get(f"/v1/permits/{num_pa}")
77
+
78
+ def near(
79
+ self, *, lat: float, lng: float, radius_m: int, **filters: Any
80
+ ) -> dict[str, Any]:
81
+ """GET /v1/permits/near?lat&lng&radius_m (PostGIS)."""
82
+ return self._c._get(
83
+ "/v1/permits/near",
84
+ params={"lat": lat, "lng": lng, "radius_m": radius_m, **filters},
85
+ )
86
+
87
+ def by_cadastre(self, *, section: str, numero: str) -> dict[str, Any]:
88
+ return self._c._get(
89
+ "/v1/permits/by-cadastre",
90
+ params={"section": section, "numero": numero},
91
+ )
92
+
93
+
94
+ class _StatsResource:
95
+ def __init__(self, client: PermisAPI) -> None:
96
+ self._c = client
97
+
98
+ def commune(self, code: str) -> dict[str, Any]:
99
+ return self._c._get(f"/v1/stats/commune/{code}")
100
+
101
+ def departement(self, code: str) -> dict[str, Any]:
102
+ return self._c._get(f"/v1/stats/departement/{code}")
103
+
104
+ def region(self, code: str) -> dict[str, Any]:
105
+ return self._c._get(f"/v1/stats/region/{code}")
106
+
107
+ def trends(
108
+ self,
109
+ *,
110
+ dep_code: str | None = None,
111
+ comm_code: str | None = None,
112
+ group_by: str = "month",
113
+ ) -> dict[str, Any]:
114
+ params = {"group_by": group_by}
115
+ if dep_code:
116
+ params["dep_code"] = dep_code
117
+ if comm_code:
118
+ params["comm_code"] = comm_code
119
+ return self._c._get("/v1/stats/trends", params=params)
120
+
121
+ def latest_day(self) -> dict[str, Any]:
122
+ return self._c._get("/v1/stats/latest-day")
123
+
124
+
125
+ class _AlertsResource:
126
+ def __init__(self, client: PermisAPI) -> None:
127
+ self._c = client
128
+
129
+ def list(self) -> list[dict[str, Any]]:
130
+ return self._c._get("/v1/alerts")
131
+
132
+ def create(
133
+ self,
134
+ *,
135
+ name: str,
136
+ center_lat: float,
137
+ center_lng: float,
138
+ radius_km: float,
139
+ webhook_url: str,
140
+ permit_types: list[str] | None = None,
141
+ min_superficie: int | None = None,
142
+ etat_pa: int | None = None,
143
+ ) -> dict[str, Any]:
144
+ """Cree une alerte. Le `webhook_secret` renvoye n'est visible QU'ICI."""
145
+ body: dict[str, Any] = {
146
+ "name": name,
147
+ "center_lat": center_lat,
148
+ "center_lng": center_lng,
149
+ "radius_km": radius_km,
150
+ "webhook_url": webhook_url,
151
+ }
152
+ if permit_types:
153
+ body["permit_types"] = permit_types
154
+ if min_superficie is not None:
155
+ body["min_superficie"] = min_superficie
156
+ if etat_pa is not None:
157
+ body["etat_pa"] = etat_pa
158
+ return self._c._post("/v1/alerts", json=body)
159
+
160
+ def get(self, alert_id: int) -> dict[str, Any]:
161
+ return self._c._get(f"/v1/alerts/{alert_id}")
162
+
163
+ def update(self, alert_id: int, **patch: Any) -> dict[str, Any]:
164
+ return self._c._patch(f"/v1/alerts/{alert_id}", json=patch)
165
+
166
+ def delete(self, alert_id: int) -> None:
167
+ self._c._delete(f"/v1/alerts/{alert_id}")
168
+
169
+ def replay(self, alert_id: int) -> dict[str, Any]:
170
+ return self._c._post(f"/v1/alerts/{alert_id}/replay")
171
+
172
+
173
+ class _MeResource:
174
+ def __init__(self, client: PermisAPI) -> None:
175
+ self._c = client
176
+
177
+ def get(self) -> dict[str, Any]:
178
+ return self._c._get("/v1/me")
179
+
180
+ def usage(self, days: int = 30) -> dict[str, Any]:
181
+ return self._c._get("/v1/me/usage", params={"days": days})
182
+
183
+ def regenerate_key(self) -> dict[str, Any]:
184
+ """Rotate la cle. La nouvelle cle n'est visible QU'UNE FOIS."""
185
+ return self._c._post("/v1/me/regenerate-key")
186
+
187
+
188
+ class PermisAPI:
189
+ """Client sync. Thread-safe pour des lectures concurrentes.
190
+
191
+ Args:
192
+ api_key: ta cle `pk_live_...` (obtenue sur https://permisapi.fr/#pricing)
193
+ base_url: override si tu self-host (defaut Railway prod)
194
+ timeout: timeout HTTP par requete (defaut 15s)
195
+ """
196
+
197
+ def __init__(
198
+ self,
199
+ api_key: str,
200
+ *,
201
+ base_url: str = DEFAULT_BASE_URL,
202
+ timeout: float = DEFAULT_TIMEOUT,
203
+ client: httpx.Client | None = None,
204
+ ) -> None:
205
+ if not api_key:
206
+ raise ValueError("api_key est requise (pk_live_... ou pk_test_...)")
207
+ self.api_key = api_key
208
+ self.base_url = base_url.rstrip("/")
209
+ self.timeout = timeout
210
+ self._http = client or httpx.Client(
211
+ base_url=self.base_url,
212
+ timeout=timeout,
213
+ headers=_build_headers(api_key),
214
+ )
215
+ self.permits = _PermitsResource(self)
216
+ self.stats = _StatsResource(self)
217
+ self.alerts = _AlertsResource(self)
218
+ self.me = _MeResource(self)
219
+
220
+ def close(self) -> None:
221
+ self._http.close()
222
+
223
+ def __enter__(self) -> PermisAPI:
224
+ return self
225
+
226
+ def __exit__(self, *exc: Any) -> None:
227
+ self.close()
228
+
229
+ # -- internal HTTP helpers --
230
+
231
+ def _get(self, path: str, *, params: dict | None = None) -> Any:
232
+ resp = self._http.get(path, params=params)
233
+ _raise_for_status(resp)
234
+ return resp.json()
235
+
236
+ def _post(self, path: str, *, json: dict | None = None) -> Any:
237
+ resp = self._http.post(path, json=json)
238
+ _raise_for_status(resp)
239
+ if resp.status_code == 204 or not resp.content:
240
+ return None
241
+ return resp.json()
242
+
243
+ def _patch(self, path: str, *, json: dict | None = None) -> Any:
244
+ resp = self._http.patch(path, json=json)
245
+ _raise_for_status(resp)
246
+ return resp.json()
247
+
248
+ def _delete(self, path: str) -> None:
249
+ resp = self._http.delete(path)
250
+ _raise_for_status(resp)
251
+
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # Async client
255
+ # ---------------------------------------------------------------------------
256
+
257
+
258
+ class _AsyncPermitsResource:
259
+ def __init__(self, client: AsyncPermisAPI) -> None:
260
+ self._c = client
261
+
262
+ async def list(self, **params: Any) -> dict[str, Any]:
263
+ return await self._c._get("/v1/permits", params=params)
264
+
265
+ async def iter_all(self, **params: Any) -> AsyncIterator[dict[str, Any]]:
266
+ page = 1
267
+ per_page = params.get("limit", 100)
268
+ while True:
269
+ p = {**params, "page": page, "limit": per_page}
270
+ data = await self.list(**p)
271
+ items = data.get("items", [])
272
+ if not items:
273
+ return
274
+ for item in items:
275
+ yield item
276
+ pages = (data.get("pagination") or {}).get("pages", 1)
277
+ if page >= pages:
278
+ return
279
+ page += 1
280
+
281
+ async def get(self, num_pa: str) -> dict[str, Any]:
282
+ return await self._c._get(f"/v1/permits/{num_pa}")
283
+
284
+ async def near(
285
+ self, *, lat: float, lng: float, radius_m: int, **filters: Any
286
+ ) -> dict[str, Any]:
287
+ return await self._c._get(
288
+ "/v1/permits/near",
289
+ params={"lat": lat, "lng": lng, "radius_m": radius_m, **filters},
290
+ )
291
+
292
+
293
+ class _AsyncStatsResource:
294
+ def __init__(self, client: AsyncPermisAPI) -> None:
295
+ self._c = client
296
+
297
+ async def commune(self, code: str) -> dict[str, Any]:
298
+ return await self._c._get(f"/v1/stats/commune/{code}")
299
+
300
+ async def departement(self, code: str) -> dict[str, Any]:
301
+ return await self._c._get(f"/v1/stats/departement/{code}")
302
+
303
+ async def region(self, code: str) -> dict[str, Any]:
304
+ return await self._c._get(f"/v1/stats/region/{code}")
305
+
306
+ async def trends(self, **params: Any) -> dict[str, Any]:
307
+ return await self._c._get("/v1/stats/trends", params=params)
308
+
309
+
310
+ class _AsyncAlertsResource:
311
+ def __init__(self, client: AsyncPermisAPI) -> None:
312
+ self._c = client
313
+
314
+ async def list(self) -> list[dict[str, Any]]:
315
+ return await self._c._get("/v1/alerts")
316
+
317
+ async def create(self, **body: Any) -> dict[str, Any]:
318
+ return await self._c._post("/v1/alerts", json=body)
319
+
320
+ async def delete(self, alert_id: int) -> None:
321
+ await self._c._delete(f"/v1/alerts/{alert_id}")
322
+
323
+
324
+ class _AsyncMeResource:
325
+ def __init__(self, client: AsyncPermisAPI) -> None:
326
+ self._c = client
327
+
328
+ async def get(self) -> dict[str, Any]:
329
+ return await self._c._get("/v1/me")
330
+
331
+ async def usage(self, days: int = 30) -> dict[str, Any]:
332
+ return await self._c._get("/v1/me/usage", params={"days": days})
333
+
334
+
335
+ class AsyncPermisAPI:
336
+ """Version async : `async with` et `await`."""
337
+
338
+ def __init__(
339
+ self,
340
+ api_key: str,
341
+ *,
342
+ base_url: str = DEFAULT_BASE_URL,
343
+ timeout: float = DEFAULT_TIMEOUT,
344
+ client: httpx.AsyncClient | None = None,
345
+ ) -> None:
346
+ if not api_key:
347
+ raise ValueError("api_key est requise (pk_live_... ou pk_test_...)")
348
+ self.api_key = api_key
349
+ self.base_url = base_url.rstrip("/")
350
+ self.timeout = timeout
351
+ self._http = client or httpx.AsyncClient(
352
+ base_url=self.base_url,
353
+ timeout=timeout,
354
+ headers=_build_headers(api_key),
355
+ )
356
+ self.permits = _AsyncPermitsResource(self)
357
+ self.stats = _AsyncStatsResource(self)
358
+ self.alerts = _AsyncAlertsResource(self)
359
+ self.me = _AsyncMeResource(self)
360
+
361
+ async def close(self) -> None:
362
+ await self._http.aclose()
363
+
364
+ async def __aenter__(self) -> AsyncPermisAPI:
365
+ return self
366
+
367
+ async def __aexit__(self, *exc: Any) -> None:
368
+ await self.close()
369
+
370
+ async def _get(self, path: str, *, params: dict | None = None) -> Any:
371
+ resp = await self._http.get(path, params=params)
372
+ _raise_for_status(resp)
373
+ return resp.json()
374
+
375
+ async def _post(self, path: str, *, json: dict | None = None) -> Any:
376
+ resp = await self._http.post(path, json=json)
377
+ _raise_for_status(resp)
378
+ if resp.status_code == 204 or not resp.content:
379
+ return None
380
+ return resp.json()
381
+
382
+ async def _delete(self, path: str) -> None:
383
+ resp = await self._http.delete(path)
384
+ _raise_for_status(resp)
@@ -0,0 +1,106 @@
1
+ """Exceptions hierarchy mirror des codes d'erreur de l'API."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+
7
+ class PermisAPIError(Exception):
8
+ """Base : toute erreur renvoyee par l'API."""
9
+
10
+ def __init__(
11
+ self,
12
+ message: str,
13
+ *,
14
+ status_code: int | None = None,
15
+ error_code: str | None = None,
16
+ detail: Any = None,
17
+ ) -> None:
18
+ super().__init__(message)
19
+ self.status_code = status_code
20
+ self.error_code = error_code
21
+ self.detail = detail
22
+
23
+
24
+ class PermisAPIUnauthorized(PermisAPIError):
25
+ """401 : cle manquante ou invalide."""
26
+
27
+
28
+ class PermisAPIForbidden(PermisAPIError):
29
+ """403 : compte desactive."""
30
+
31
+
32
+ class PermisAPIRateLimited(PermisAPIError):
33
+ """429 : quota minute atteint (retry apres X secondes)."""
34
+
35
+ def __init__(
36
+ self,
37
+ message: str,
38
+ *,
39
+ retry_after: int | None = None,
40
+ status_code: int | None = None,
41
+ error_code: str | None = None,
42
+ detail: Any = None,
43
+ ) -> None:
44
+ super().__init__(
45
+ message,
46
+ status_code=status_code,
47
+ error_code=error_code,
48
+ detail=detail,
49
+ )
50
+ self.retry_after = retry_after
51
+
52
+
53
+ class PermisAPIQuotaExceeded(PermisAPIRateLimited):
54
+ """Quota mensuel depasse : il faut upgrade ou attendre le prochain mois."""
55
+
56
+
57
+ class PermisAPIValidationError(PermisAPIError):
58
+ """400 : un parametre n'est pas au bon format."""
59
+
60
+
61
+ class PermisAPINotFound(PermisAPIError):
62
+ """404 : permit ou ressource inexistante."""
63
+
64
+
65
+ def error_from_response(status_code: int, payload: dict[str, Any]) -> PermisAPIError:
66
+ """Construit la bonne sous-classe selon le status + payload de l'API."""
67
+ error_code = payload.get("error")
68
+ message = payload.get("message") or payload.get("detail") or "Erreur API"
69
+ detail = payload.get("detail")
70
+
71
+ if status_code == 401:
72
+ return PermisAPIUnauthorized(
73
+ message, status_code=status_code, error_code=error_code, detail=detail
74
+ )
75
+ if status_code == 403:
76
+ return PermisAPIForbidden(
77
+ message, status_code=status_code, error_code=error_code, detail=detail
78
+ )
79
+ if status_code == 404:
80
+ return PermisAPINotFound(
81
+ message, status_code=status_code, error_code=error_code, detail=detail
82
+ )
83
+ if status_code == 429:
84
+ retry_after = payload.get("retry_after")
85
+ if error_code == "quota_exceeded":
86
+ return PermisAPIQuotaExceeded(
87
+ message,
88
+ retry_after=retry_after,
89
+ status_code=status_code,
90
+ error_code=error_code,
91
+ detail=detail,
92
+ )
93
+ return PermisAPIRateLimited(
94
+ message,
95
+ retry_after=retry_after,
96
+ status_code=status_code,
97
+ error_code=error_code,
98
+ detail=detail,
99
+ )
100
+ if status_code == 400:
101
+ return PermisAPIValidationError(
102
+ message, status_code=status_code, error_code=error_code, detail=detail
103
+ )
104
+ return PermisAPIError(
105
+ message, status_code=status_code, error_code=error_code, detail=detail
106
+ )
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: permisapi-client
3
+ Version: 0.1.0
4
+ Summary: Client Python officiel pour PermisAPI : permis de construire 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 pour [PermisAPI](https://permisapi.fr) : accede aux 311k+ permis de construire France via une API REST moderne.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install permisapi-client
37
+ ```
38
+
39
+ ## Quickstart
40
+
41
+ ```python
42
+ from permisapi_client import PermisAPI
43
+
44
+ client = PermisAPI(api_key="pk_live_...") # obtenue sur https://permisapi.fr/#pricing
45
+
46
+ # Liste
47
+ data = client.permits.list(dep_code="75", limit=10)
48
+ for permit in data["items"]:
49
+ print(permit["num_pa"], permit["adr_localite_ter"])
50
+
51
+ # Recherche geographique (PostGIS)
52
+ nearby = client.permits.near(lat=48.85, lng=2.35, radius_m=500)
53
+
54
+ # Detail
55
+ permit = client.permits.get("PC 075 115 24 B0001")
56
+
57
+ # Stats par commune
58
+ stats = client.stats.commune("75115")
59
+ print(stats["total"], stats["granted"])
60
+
61
+ # Itere sur toutes les pages
62
+ for p in client.permits.iter_all(dep_code="75"):
63
+ print(p["num_pa"])
64
+ ```
65
+
66
+ ## Async
67
+
68
+ ```python
69
+ import asyncio
70
+ from permisapi_client import AsyncPermisAPI
71
+
72
+ async def main():
73
+ async with AsyncPermisAPI(api_key="pk_live_...") as client:
74
+ data = await client.permits.list(dep_code="75")
75
+ async for p in client.permits.iter_all(dep_code="75"):
76
+ print(p["num_pa"])
77
+
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ ## Alertes webhook
82
+
83
+ ```python
84
+ alert = client.alerts.create(
85
+ name="Paris 18e renovations",
86
+ center_lat=48.88,
87
+ center_lng=2.35,
88
+ radius_km=3,
89
+ permit_types=["PC_LOGEMENT"],
90
+ webhook_url="https://tonapp.fr/permisapi-hook",
91
+ )
92
+ # Le webhook_secret n'est renvoye QU'ICI. Stocke-le pour verifier les HMAC.
93
+ print(alert["webhook_secret"])
94
+ ```
95
+
96
+ ## Gestion des erreurs
97
+
98
+ ```python
99
+ from permisapi_client import (
100
+ PermisAPI,
101
+ PermisAPIRateLimited,
102
+ PermisAPIQuotaExceeded,
103
+ PermisAPIUnauthorized,
104
+ )
105
+
106
+ try:
107
+ client.permits.list()
108
+ except PermisAPIUnauthorized:
109
+ print("Cle API invalide")
110
+ except PermisAPIQuotaExceeded as e:
111
+ print(f"Quota mensuel atteint, retry dans {e.retry_after}s")
112
+ except PermisAPIRateLimited as e:
113
+ print(f"Burst limit, retry dans {e.retry_after}s")
114
+ ```
115
+
116
+ ## Licence
117
+
118
+ MIT. Source : SDES, base Sitadel, data.gouv.fr (Licence Ouverte Etalab).
119
+
120
+ ## Liens
121
+
122
+ - Documentation : https://permisapi.fr/docs
123
+ - Reference API complete : https://api.permisapi.fr/docs
124
+ - Changelog : https://permisapi.fr/changelog
@@ -0,0 +1,6 @@
1
+ permisapi_client/__init__.py,sha256=9pbg6c9oAVlE9foYvhjil_vt5379nASvUdb58fLVkvg,967
2
+ permisapi_client/client.py,sha256=oK09zY_pFdWbRheIFPhtF0pLuaCwjVWj2UpvTmgNA7U,12632
3
+ permisapi_client/exceptions.py,sha256=5nqRA28mwoB3gDFoWyDuaxS8R4fXCdI9j90F_QNN84s,3284
4
+ permisapi_client-0.1.0.dist-info/METADATA,sha256=eJ80i8aKQUnFe6ngDDzLtgUpulPhW_oI4t5QG5IwHe0,3457
5
+ permisapi_client-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ permisapi_client-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any