formbridge-sdk 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.
formbridge/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """FormBridge Python SDK — async and sync HTTP client."""
2
+
3
+ from formbridge.client import FormBridgeClient, FormBridgeClientSync
4
+ from formbridge.errors import FormBridgeError
5
+ from formbridge.types import Actor, FieldsResult, Submission
6
+
7
+ __all__ = [
8
+ "FormBridgeClient",
9
+ "FormBridgeClientSync",
10
+ "FormBridgeError",
11
+ "Actor",
12
+ "FieldsResult",
13
+ "Submission",
14
+ ]
formbridge/client.py ADDED
@@ -0,0 +1,387 @@
1
+ """FormBridge async and sync HTTP clients.
2
+
3
+ Usage::
4
+
5
+ async with FormBridgeClient() as client:
6
+ sub = await client.create_submission("vendor-onboarding", fields={"company": "Acme"})
7
+ result = await client.set_fields(sub.intake_id, sub.submission_id, sub.resume_token, {"email": "a@b.com"})
8
+ final = await client.submit(sub.intake_id, sub.submission_id, result.resume_token)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import logging
15
+ import os
16
+ from typing import Any, Dict, Optional
17
+
18
+ try:
19
+ import httpx
20
+ except ImportError:
21
+ raise ImportError("httpx is required. Install with: pip install formbridge-sdk")
22
+
23
+ from formbridge.errors import FormBridgeError
24
+ from formbridge.types import Actor, FieldsResult, Submission
25
+
26
+ logger = logging.getLogger("formbridge.client")
27
+
28
+ _DEFAULT_URL = "http://localhost:3000"
29
+ _DEFAULT_TIMEOUT = 10.0
30
+ _RETRY_BACKOFFS = [0.5, 1.0, 2.0]
31
+ _RETRYABLE_STATUS = {429, 500, 502, 503, 504}
32
+
33
+
34
+ def _serialize_actor(actor: Optional[Actor]) -> Optional[Dict[str, Any]]:
35
+ if actor is None:
36
+ return None
37
+ d: Dict[str, Any] = {"kind": actor.kind, "id": actor.id}
38
+ if actor.name is not None:
39
+ d["name"] = actor.name
40
+ return d
41
+
42
+
43
+ class FormBridgeClient:
44
+ """Async HTTP client for FormBridge API.
45
+
46
+ Parameters:
47
+ url: Base URL. Defaults to ``FORMBRIDGE_URL`` env var or ``http://localhost:3000``.
48
+ api_key: API key for Bearer auth. Defaults to ``FORMBRIDGE_API_KEY`` env var.
49
+ timeout: Request timeout in seconds. Defaults to ``FORMBRIDGE_TIMEOUT`` env var or 10.
50
+ max_retries: Max retry attempts on 429/5xx (default 3).
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ url: Optional[str] = None,
56
+ api_key: Optional[str] = None,
57
+ timeout: Optional[float] = None,
58
+ max_retries: int = 3,
59
+ ) -> None:
60
+ self._url = (url or os.environ.get("FORMBRIDGE_URL", _DEFAULT_URL)).rstrip("/")
61
+ self._api_key = api_key or os.environ.get("FORMBRIDGE_API_KEY", "")
62
+ raw_timeout = timeout if timeout is not None else os.environ.get("FORMBRIDGE_TIMEOUT")
63
+ self._timeout = float(raw_timeout) if raw_timeout is not None else _DEFAULT_TIMEOUT
64
+ self._max_retries = max_retries
65
+
66
+ headers: Dict[str, str] = {"Content-Type": "application/json"}
67
+ if self._api_key:
68
+ headers["Authorization"] = f"Bearer {self._api_key}"
69
+
70
+ self._http = httpx.AsyncClient(
71
+ base_url=self._url,
72
+ headers=headers,
73
+ timeout=self._timeout,
74
+ )
75
+
76
+ async def __aenter__(self) -> "FormBridgeClient":
77
+ return self
78
+
79
+ async def __aexit__(self, *args: object) -> None:
80
+ await self.close()
81
+
82
+ async def close(self) -> None:
83
+ """Close the underlying HTTP client."""
84
+ await self._http.aclose()
85
+
86
+ # ── Public API ────────────────────────────────────────────────────
87
+
88
+ async def create_submission(
89
+ self,
90
+ intake_id: str,
91
+ *,
92
+ fields: Optional[Dict[str, Any]] = None,
93
+ actor: Optional[Actor] = None,
94
+ idempotency_key: Optional[str] = None,
95
+ ) -> Submission:
96
+ """Create a new submission.
97
+
98
+ Args:
99
+ intake_id: The intake definition ID.
100
+ fields: Optional initial field values.
101
+ actor: Optional actor performing the action.
102
+ idempotency_key: Optional idempotency key for deduplication.
103
+
104
+ Returns:
105
+ Submission with id, state, resume_token, etc.
106
+
107
+ Raises:
108
+ FormBridgeError: On API or connectivity error.
109
+ """
110
+ body: Dict[str, Any] = {}
111
+ if fields:
112
+ body["fields"] = fields
113
+ if actor:
114
+ body["actor"] = _serialize_actor(actor)
115
+ if idempotency_key:
116
+ body["idempotencyKey"] = idempotency_key
117
+
118
+ data = await self._request("POST", f"/intake/{intake_id}/submissions", json_data=body)
119
+ sub = Submission.from_response(data)
120
+ # API may not return intakeId on create; fill it in
121
+ if not sub.intake_id:
122
+ sub = Submission(
123
+ submission_id=sub.submission_id,
124
+ intake_id=intake_id,
125
+ state=sub.state,
126
+ resume_token=sub.resume_token,
127
+ fields=sub.fields,
128
+ missing_fields=sub.missing_fields,
129
+ schema=sub.schema,
130
+ created_at=sub.created_at,
131
+ updated_at=sub.updated_at,
132
+ raw=sub.raw,
133
+ )
134
+ return sub
135
+
136
+ async def set_fields(
137
+ self,
138
+ intake_id: str,
139
+ submission_id: str,
140
+ resume_token: str,
141
+ fields: Dict[str, Any],
142
+ actor: Optional[Actor] = None,
143
+ ) -> FieldsResult:
144
+ """Update fields on a submission.
145
+
146
+ Args:
147
+ intake_id: The intake definition ID.
148
+ submission_id: The submission ID.
149
+ resume_token: Current resume token.
150
+ fields: Field values to set.
151
+ actor: Optional actor performing the action.
152
+
153
+ Returns:
154
+ FieldsResult with new state, resume_token (may rotate), missing_fields.
155
+
156
+ Raises:
157
+ FormBridgeError: On API or connectivity error.
158
+ """
159
+ body: Dict[str, Any] = {
160
+ "resumeToken": resume_token,
161
+ "fields": fields,
162
+ }
163
+ if actor:
164
+ body["actor"] = _serialize_actor(actor)
165
+
166
+ data = await self._request(
167
+ "PATCH", f"/intake/{intake_id}/submissions/{submission_id}", json_data=body
168
+ )
169
+ return FieldsResult.from_response(data)
170
+
171
+ async def submit(
172
+ self,
173
+ intake_id: str,
174
+ submission_id: str,
175
+ resume_token: str,
176
+ *,
177
+ actor: Optional[Actor] = None,
178
+ ) -> Submission:
179
+ """Submit a submission for processing.
180
+
181
+ Args:
182
+ intake_id: The intake definition ID.
183
+ submission_id: The submission ID.
184
+ resume_token: Current resume token.
185
+ actor: Optional actor performing the action.
186
+
187
+ Returns:
188
+ Final Submission state.
189
+
190
+ Raises:
191
+ FormBridgeError: On API or connectivity error.
192
+ """
193
+ body: Dict[str, Any] = {"resumeToken": resume_token}
194
+ if actor:
195
+ body["actor"] = _serialize_actor(actor)
196
+
197
+ data = await self._request(
198
+ "POST", f"/intake/{intake_id}/submissions/{submission_id}/submit", json_data=body
199
+ )
200
+ return Submission.from_response(data)
201
+
202
+ async def get_submission(
203
+ self,
204
+ intake_id: str,
205
+ submission_id: str,
206
+ ) -> Submission:
207
+ """Get a submission by ID.
208
+
209
+ Args:
210
+ intake_id: The intake definition ID.
211
+ submission_id: The submission ID.
212
+
213
+ Returns:
214
+ Submission details.
215
+
216
+ Raises:
217
+ FormBridgeError: On API or connectivity error.
218
+ """
219
+ data = await self._request(
220
+ "GET", f"/intake/{intake_id}/submissions/{submission_id}"
221
+ )
222
+ return Submission.from_response(data)
223
+
224
+ # ── Internals ─────────────────────────────────────────────────────
225
+
226
+ async def _request(
227
+ self,
228
+ method: str,
229
+ path: str,
230
+ *,
231
+ json_data: Optional[Any] = None,
232
+ ) -> Dict[str, Any]:
233
+ """Make HTTP request with retry and graceful error handling."""
234
+ last_exc: Optional[Exception] = None
235
+ backoffs = _RETRY_BACKOFFS[: self._max_retries]
236
+
237
+ for attempt in range(1 + len(backoffs)):
238
+ try:
239
+ resp = await self._http.request(method, path, json=json_data)
240
+
241
+ if resp.status_code in _RETRYABLE_STATUS and attempt < len(backoffs):
242
+ logger.warning(
243
+ "FormBridge returned %d (attempt %d/%d), retrying in %.1fs",
244
+ resp.status_code,
245
+ attempt + 1,
246
+ len(backoffs) + 1,
247
+ backoffs[attempt],
248
+ )
249
+ await asyncio.sleep(backoffs[attempt])
250
+ continue
251
+
252
+ data = resp.json()
253
+
254
+ if resp.status_code >= 400:
255
+ error_info = data.get("error", {}) if isinstance(data, dict) else {}
256
+ raise FormBridgeError(
257
+ error_info.get("message", f"HTTP {resp.status_code}"),
258
+ status_code=resp.status_code,
259
+ error_type=error_info.get("type"),
260
+ response_data=data,
261
+ )
262
+
263
+ return data
264
+
265
+ except FormBridgeError:
266
+ raise
267
+ except (httpx.ConnectError, httpx.TimeoutException, httpx.ConnectTimeout) as exc:
268
+ last_exc = exc
269
+ if attempt < len(backoffs):
270
+ logger.warning(
271
+ "FormBridge connection error (attempt %d/%d): %s, retrying in %.1fs",
272
+ attempt + 1,
273
+ len(backoffs) + 1,
274
+ exc,
275
+ backoffs[attempt],
276
+ )
277
+ await asyncio.sleep(backoffs[attempt])
278
+ continue
279
+ raise FormBridgeError(
280
+ f"Connection failed: {exc}",
281
+ is_connectivity_error=True,
282
+ ) from exc
283
+ except Exception as exc:
284
+ raise FormBridgeError(
285
+ f"Unexpected error: {exc}",
286
+ is_connectivity_error=True,
287
+ ) from exc
288
+
289
+ # Should not reach here
290
+ if last_exc:
291
+ raise FormBridgeError(
292
+ f"Connection failed after retries: {last_exc}",
293
+ is_connectivity_error=True,
294
+ ) from last_exc
295
+ raise RuntimeError("Retry loop exhausted unexpectedly")
296
+
297
+
298
+ class FormBridgeClientSync:
299
+ """Synchronous wrapper around FormBridgeClient.
300
+
301
+ Works without an existing event loop. Creates its own loop internally.
302
+
303
+ Usage::
304
+
305
+ client = FormBridgeClientSync(api_key="sk-...")
306
+ sub = client.create_submission("vendor-onboarding", fields={"company": "Acme"})
307
+ client.close()
308
+ """
309
+
310
+ def __init__(self, **kwargs: Any) -> None:
311
+ self._async_client = FormBridgeClient(**kwargs)
312
+
313
+ def __enter__(self) -> "FormBridgeClientSync":
314
+ return self
315
+
316
+ def __exit__(self, *args: object) -> None:
317
+ self.close()
318
+
319
+ def close(self) -> None:
320
+ """Close the underlying HTTP client."""
321
+ self._run(self._async_client.close())
322
+
323
+ def create_submission(
324
+ self,
325
+ intake_id: str,
326
+ *,
327
+ fields: Optional[Dict[str, Any]] = None,
328
+ actor: Optional[Actor] = None,
329
+ idempotency_key: Optional[str] = None,
330
+ ) -> Submission:
331
+ """Create a new submission. See :meth:`FormBridgeClient.create_submission`."""
332
+ return self._run(
333
+ self._async_client.create_submission(
334
+ intake_id, fields=fields, actor=actor, idempotency_key=idempotency_key
335
+ )
336
+ )
337
+
338
+ def set_fields(
339
+ self,
340
+ intake_id: str,
341
+ submission_id: str,
342
+ resume_token: str,
343
+ fields: Dict[str, Any],
344
+ actor: Optional[Actor] = None,
345
+ ) -> FieldsResult:
346
+ """Update fields on a submission. See :meth:`FormBridgeClient.set_fields`."""
347
+ return self._run(
348
+ self._async_client.set_fields(intake_id, submission_id, resume_token, fields, actor)
349
+ )
350
+
351
+ def submit(
352
+ self,
353
+ intake_id: str,
354
+ submission_id: str,
355
+ resume_token: str,
356
+ *,
357
+ actor: Optional[Actor] = None,
358
+ ) -> Submission:
359
+ """Submit a submission. See :meth:`FormBridgeClient.submit`."""
360
+ return self._run(
361
+ self._async_client.submit(intake_id, submission_id, resume_token, actor=actor)
362
+ )
363
+
364
+ def get_submission(
365
+ self,
366
+ intake_id: str,
367
+ submission_id: str,
368
+ ) -> Submission:
369
+ """Get a submission by ID. See :meth:`FormBridgeClient.get_submission`."""
370
+ return self._run(self._async_client.get_submission(intake_id, submission_id))
371
+
372
+ @staticmethod
373
+ def _run(coro: Any) -> Any:
374
+ """Run a coroutine in a new event loop (safe from any context)."""
375
+ try:
376
+ loop = asyncio.get_running_loop()
377
+ except RuntimeError:
378
+ loop = None
379
+
380
+ if loop and loop.is_running():
381
+ # We're inside an async context — use a thread
382
+ import concurrent.futures
383
+
384
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
385
+ return pool.submit(asyncio.run, coro).result()
386
+ else:
387
+ return asyncio.run(coro)
formbridge/errors.py ADDED
@@ -0,0 +1,31 @@
1
+ """Error types for FormBridge SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Optional
6
+
7
+
8
+ class FormBridgeError(Exception):
9
+ """Base error for FormBridge SDK.
10
+
11
+ Attributes:
12
+ status_code: HTTP status code (None for connectivity errors).
13
+ error_type: Error type from API response (e.g. 'not_found').
14
+ is_connectivity_error: True if the error is due to a connection failure.
15
+ response_data: Raw response body if available.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ message: str,
21
+ *,
22
+ status_code: Optional[int] = None,
23
+ error_type: Optional[str] = None,
24
+ is_connectivity_error: bool = False,
25
+ response_data: Optional[Dict[str, Any]] = None,
26
+ ) -> None:
27
+ super().__init__(message)
28
+ self.status_code = status_code
29
+ self.error_type = error_type
30
+ self.is_connectivity_error = is_connectivity_error
31
+ self.response_data = response_data
formbridge/py.typed ADDED
File without changes
formbridge/types.py ADDED
@@ -0,0 +1,73 @@
1
+ """Response types for FormBridge SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Actor:
11
+ """Submission actor."""
12
+
13
+ kind: str
14
+ id: str
15
+ name: Optional[str] = None
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Submission:
20
+ """Submission returned by the API."""
21
+
22
+ submission_id: str
23
+ intake_id: str
24
+ state: str
25
+ resume_token: Optional[str] = None
26
+ fields: Dict[str, Any] = field(default_factory=dict)
27
+ missing_fields: Optional[List[str]] = None
28
+ schema: Optional[Dict[str, Any]] = None
29
+ created_at: Optional[str] = None
30
+ updated_at: Optional[str] = None
31
+ raw: Dict[str, Any] = field(default_factory=dict)
32
+
33
+ @classmethod
34
+ def from_response(cls, data: Dict[str, Any]) -> "Submission":
35
+ """Parse API response into a Submission."""
36
+ return cls(
37
+ submission_id=data.get("submissionId", ""),
38
+ intake_id=data.get("intakeId", ""),
39
+ state=data.get("state", ""),
40
+ resume_token=data.get("resumeToken"),
41
+ fields=data.get("fields", {}),
42
+ missing_fields=data.get("missingFields"),
43
+ schema=data.get("schema"),
44
+ created_at=data.get("metadata", {}).get("createdAt")
45
+ if isinstance(data.get("metadata"), dict)
46
+ else data.get("createdAt"),
47
+ updated_at=data.get("metadata", {}).get("updatedAt")
48
+ if isinstance(data.get("metadata"), dict)
49
+ else data.get("updatedAt"),
50
+ raw=data,
51
+ )
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class FieldsResult:
56
+ """Result from set_fields."""
57
+
58
+ submission_id: str
59
+ state: str
60
+ resume_token: str
61
+ missing_fields: Optional[List[str]] = None
62
+ raw: Dict[str, Any] = field(default_factory=dict)
63
+
64
+ @classmethod
65
+ def from_response(cls, data: Dict[str, Any]) -> "FieldsResult":
66
+ """Parse API response into a FieldsResult."""
67
+ return cls(
68
+ submission_id=data.get("submissionId", ""),
69
+ state=data.get("state", ""),
70
+ resume_token=data.get("resumeToken", ""),
71
+ missing_fields=data.get("missingFields"),
72
+ raw=data,
73
+ )
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: formbridge-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for FormBridge — async and sync HTTP client
5
+ Author: FormBridge Team
6
+ License-Expression: MIT
7
+ Keywords: formbridge,forms,sdk,submissions
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Typing :: Typed
12
+ Requires-Python: >=3.9
13
+ Requires-Dist: httpx>=0.24.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
16
+ Requires-Dist: pytest>=7.0; extra == 'dev'
17
+ Requires-Dist: respx>=0.21; extra == 'dev'
18
+ Requires-Dist: ruff>=0.1; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # formbridge-sdk
22
+
23
+ Python SDK for [FormBridge](https://github.com/agentkitai/formbridge) — async and sync HTTP client for managing form submissions.
24
+
25
+ ## Installation
26
+
27
+ This SDK is not yet published to PyPI. Install it from source:
28
+
29
+ ```bash
30
+ git clone https://github.com/agentkitai/formbridge.git
31
+ pip install ./formbridge/sdk/python
32
+ ```
33
+
34
+ ## Quick Start (Async)
35
+
36
+ ```python
37
+ import asyncio
38
+ from formbridge import FormBridgeClient, Actor
39
+
40
+ async def main():
41
+ async with FormBridgeClient(
42
+ url="http://localhost:3000",
43
+ api_key="your-api-key",
44
+ ) as client:
45
+ # Create a submission with initial fields
46
+ sub = await client.create_submission(
47
+ "vendor-onboarding",
48
+ fields={"company_name": "Acme Corp"},
49
+ actor=Actor(kind="agent", id="agent-1", name="Onboarding Bot"),
50
+ )
51
+ print(f"Created: {sub.submission_id}, state={sub.state}")
52
+ print(f"Missing fields: {sub.missing_fields}")
53
+
54
+ # Add more fields (resume_token rotates on each call)
55
+ result = await client.set_fields(
56
+ sub.intake_id,
57
+ sub.submission_id,
58
+ sub.resume_token,
59
+ {"contact_email": "alice@acme.com", "phone": "+1-555-0100"},
60
+ )
61
+ print(f"Updated: state={result.state}, missing={result.missing_fields}")
62
+
63
+ # Submit when all fields are filled
64
+ final = await client.submit(
65
+ sub.intake_id,
66
+ sub.submission_id,
67
+ result.resume_token,
68
+ )
69
+ print(f"Submitted: state={final.state}")
70
+
71
+ # Retrieve a submission
72
+ fetched = await client.get_submission(sub.intake_id, sub.submission_id)
73
+ print(f"Fields: {fetched.fields}")
74
+
75
+ asyncio.run(main())
76
+ ```
77
+
78
+ ## Quick Start (Sync)
79
+
80
+ ```python
81
+ from formbridge import FormBridgeClientSync, Actor
82
+
83
+ with FormBridgeClientSync(api_key="your-api-key") as client:
84
+ sub = client.create_submission(
85
+ "vendor-onboarding",
86
+ fields={"company_name": "Acme Corp"},
87
+ actor=Actor(kind="agent", id="agent-1"),
88
+ )
89
+ print(f"Created: {sub.submission_id}")
90
+ ```
91
+
92
+ ## Configuration
93
+
94
+ | Parameter | Env Var | Default |
95
+ |-----------|---------|---------|
96
+ | `url` | `FORMBRIDGE_URL` | `http://localhost:3000` |
97
+ | `api_key` | `FORMBRIDGE_API_KEY` | — |
98
+ | `timeout` | `FORMBRIDGE_TIMEOUT` | `10` (seconds) |
99
+
100
+ ## Error Handling
101
+
102
+ ```python
103
+ from formbridge import FormBridgeClient, FormBridgeError
104
+
105
+ async with FormBridgeClient() as client:
106
+ try:
107
+ sub = await client.get_submission("intake-1", "sub-123")
108
+ except FormBridgeError as e:
109
+ if e.is_connectivity_error:
110
+ print("Server unreachable — degrade gracefully")
111
+ elif e.status_code == 404:
112
+ print("Not found")
113
+ else:
114
+ print(f"API error: {e.error_type} — {e}")
115
+ ```
116
+
117
+ ## Retry Behavior
118
+
119
+ - **Retries on:** 429 (rate limited), 500, 502, 503, 504
120
+ - **Does NOT retry on:** 400, 401, 403, 404 (client errors)
121
+ - **Backoff:** 0.5s → 1.0s → 2.0s (exponential)
122
+ - **Max retries:** 3 (configurable via `max_retries`)
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,8 @@
1
+ formbridge/__init__.py,sha256=k4W7Z4XnpDK3P5mxLINfpf1xLCx4ccRSxi_43ioHDG4,392
2
+ formbridge/client.py,sha256=HwlRJAFiEG4XqYAdeVjofA_mrduC_CWbmT5cFIC6lxQ,13327
3
+ formbridge/errors.py,sha256=sXh5Mt8MlvjJKN40xy8GA6RvwEHaAF07X6vlFdO9YmU,998
4
+ formbridge/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ formbridge/types.py,sha256=-K3M0HyLAocvAx9piOsVVxbSLve1GvNNS63eJUeO95I,2297
6
+ formbridge_sdk-0.1.0.dist-info/METADATA,sha256=PZKEueirSZgL3uPF61SafZljG5IcxomQA4G0oEV0MnI,3730
7
+ formbridge_sdk-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
8
+ formbridge_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any