meibel 0.1.0b1__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.
- meibel-0.1.0b1/PKG-INFO +86 -0
- meibel-0.1.0b1/README.md +57 -0
- meibel-0.1.0b1/meibel/__init__.py +18 -0
- meibel-0.1.0b1/meibel/_http.py +309 -0
- meibel-0.1.0b1/meibel/_pagination.py +170 -0
- meibel-0.1.0b1/meibel/_streaming.py +226 -0
- meibel-0.1.0b1/meibel/_upload.py +104 -0
- meibel-0.1.0b1/meibel/client.py +127 -0
- meibel-0.1.0b1/meibel/exceptions.py +86 -0
- meibel-0.1.0b1/meibel/models.py +1546 -0
- meibel-0.1.0b1/meibel/py.typed +0 -0
- meibel-0.1.0b1/meibel/resources/__init__.py +85 -0
- meibel-0.1.0b1/meibel/resources/blueprints.py +390 -0
- meibel-0.1.0b1/meibel/resources/blueprints_executions.py +298 -0
- meibel-0.1.0b1/meibel/resources/blueprints_instances.py +612 -0
- meibel-0.1.0b1/meibel/resources/confidence_scoring.py +214 -0
- meibel-0.1.0b1/meibel/resources/content.py +326 -0
- meibel-0.1.0b1/meibel/resources/data_element_metadata.py +150 -0
- meibel-0.1.0b1/meibel/resources/data_elements.py +271 -0
- meibel-0.1.0b1/meibel/resources/datasources.py +209 -0
- meibel-0.1.0b1/meibel/resources/datasources_content.py +447 -0
- meibel-0.1.0b1/meibel/resources/datasources_dataelements.py +398 -0
- meibel-0.1.0b1/meibel/resources/datasources_metadata_model_catalog.py +115 -0
- meibel-0.1.0b1/meibel/resources/datasources_rag.py +432 -0
- meibel-0.1.0b1/meibel/resources/datasources_tag.py +592 -0
- meibel-0.1.0b1/meibel/resources/documents.py +283 -0
- meibel-0.1.0b1/meibel/resources/metadata_configuration.py +170 -0
- meibel-0.1.0b1/meibel/resources/metadata_model_catalog.py +115 -0
- meibel-0.1.0b1/meibel/resources/tag_descriptions.py +213 -0
- meibel-0.1.0b1/meibel.egg-info/PKG-INFO +86 -0
- meibel-0.1.0b1/meibel.egg-info/SOURCES.txt +34 -0
- meibel-0.1.0b1/meibel.egg-info/dependency_links.txt +1 -0
- meibel-0.1.0b1/meibel.egg-info/requires.txt +8 -0
- meibel-0.1.0b1/meibel.egg-info/top_level.txt +2 -0
- meibel-0.1.0b1/pyproject.toml +55 -0
- meibel-0.1.0b1/setup.cfg +4 -0
meibel-0.1.0b1/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meibel
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: The Meibel API provides document parsing, datasource management, and AI agent orchestration.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://docs.meibel.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/meibel-ai/meibel-python
|
|
8
|
+
Project-URL: Documentation, https://docs.meibel.ai/sdk/python
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/meibel-ai/meibel-python/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.8
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: httpx>=0.25.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
27
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
29
|
+
|
|
30
|
+
# Meibel Python SDK
|
|
31
|
+
|
|
32
|
+
The official Python SDK for the [Meibel API](https://docs.meibel.ai). Provides document parsing, datasource management, and AI agent orchestration.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install meibel==0.1.0b1
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from meibel import MeibelClient
|
|
44
|
+
|
|
45
|
+
client = MeibelClient(
|
|
46
|
+
api_key="your-api-key",
|
|
47
|
+
base_url="https://api.meibel.ai/v2",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Parse a document
|
|
51
|
+
with open("document.pdf", "rb") as f:
|
|
52
|
+
result = client.documents.parse_document(file=f)
|
|
53
|
+
print(result.job_id)
|
|
54
|
+
|
|
55
|
+
# Process a document synchronously (waits for completion)
|
|
56
|
+
with open("document.pdf", "rb") as f:
|
|
57
|
+
result = client.documents.process_document(file=f)
|
|
58
|
+
print(result)
|
|
59
|
+
|
|
60
|
+
# List datasources
|
|
61
|
+
datasources = client.datasources.list_datasources()
|
|
62
|
+
for ds in datasources.items:
|
|
63
|
+
print(ds.name)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Async Usage
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from meibel import AsyncMeibelClient
|
|
70
|
+
|
|
71
|
+
client = AsyncMeibelClient(
|
|
72
|
+
api_key="your-api-key",
|
|
73
|
+
base_url="https://api.meibel.ai/v2",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
result = await client.documents.process_document(file=open("doc.pdf", "rb"))
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Documentation
|
|
80
|
+
|
|
81
|
+
- [API Reference](https://docs.meibel.ai/api-reference/overview)
|
|
82
|
+
- [SDK Guide](https://docs.meibel.ai/sdk/python)
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
meibel-0.1.0b1/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Meibel Python SDK
|
|
2
|
+
|
|
3
|
+
The official Python SDK for the [Meibel API](https://docs.meibel.ai). Provides document parsing, datasource management, and AI agent orchestration.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install meibel==0.1.0b1
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from meibel import MeibelClient
|
|
15
|
+
|
|
16
|
+
client = MeibelClient(
|
|
17
|
+
api_key="your-api-key",
|
|
18
|
+
base_url="https://api.meibel.ai/v2",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Parse a document
|
|
22
|
+
with open("document.pdf", "rb") as f:
|
|
23
|
+
result = client.documents.parse_document(file=f)
|
|
24
|
+
print(result.job_id)
|
|
25
|
+
|
|
26
|
+
# Process a document synchronously (waits for completion)
|
|
27
|
+
with open("document.pdf", "rb") as f:
|
|
28
|
+
result = client.documents.process_document(file=f)
|
|
29
|
+
print(result)
|
|
30
|
+
|
|
31
|
+
# List datasources
|
|
32
|
+
datasources = client.datasources.list_datasources()
|
|
33
|
+
for ds in datasources.items:
|
|
34
|
+
print(ds.name)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Async Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from meibel import AsyncMeibelClient
|
|
41
|
+
|
|
42
|
+
client = AsyncMeibelClient(
|
|
43
|
+
api_key="your-api-key",
|
|
44
|
+
base_url="https://api.meibel.ai/v2",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
result = await client.documents.process_document(file=open("doc.pdf", "rb"))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
- [API Reference](https://docs.meibel.ai/api-reference/overview)
|
|
53
|
+
- [SDK Guide](https://docs.meibel.ai/sdk/python)
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
meibel - The Meibel API provides document parsing, datasource management, and AI agent orchestration.
|
|
3
|
+
|
|
4
|
+
Generated by Forge SDK Generator.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0-beta.1"
|
|
8
|
+
|
|
9
|
+
from .client import MeibelClient
|
|
10
|
+
from .client import AsyncMeibelClient
|
|
11
|
+
from .models import *
|
|
12
|
+
from .exceptions import *
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"MeibelClient",
|
|
16
|
+
"AsyncMeibelClient",
|
|
17
|
+
"__version__",
|
|
18
|
+
]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP Client Module
|
|
3
|
+
|
|
4
|
+
Provides sync and async HTTP clients using httpx.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from typing import Any, Dict, Optional, TypeVar, Type, Union
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from .exceptions import ApiError, AuthenticationError, RateLimitError, NotFoundError
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T", bound=BaseModel)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HttpClient:
|
|
19
|
+
"""Synchronous HTTP client."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
base_url: str,
|
|
24
|
+
api_key: Optional[str] = None,
|
|
25
|
+
bearer_token: Optional[str] = None,
|
|
26
|
+
timeout: float = 30.0,
|
|
27
|
+
headers: Optional[Dict[str, str]] = None,
|
|
28
|
+
):
|
|
29
|
+
self.base_url = base_url.rstrip("/")
|
|
30
|
+
self._timeout = timeout
|
|
31
|
+
self._headers = headers or {}
|
|
32
|
+
|
|
33
|
+
if api_key:
|
|
34
|
+
self._headers["Meibel-API-Key"] = api_key
|
|
35
|
+
if bearer_token:
|
|
36
|
+
self._headers["Authorization"] = f"Bearer {bearer_token}"
|
|
37
|
+
|
|
38
|
+
self._client = httpx.Client(
|
|
39
|
+
base_url=self.base_url,
|
|
40
|
+
timeout=timeout,
|
|
41
|
+
headers=self._headers,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def close(self) -> None:
|
|
45
|
+
"""Close the HTTP client."""
|
|
46
|
+
self._client.close()
|
|
47
|
+
|
|
48
|
+
def __enter__(self) -> "HttpClient":
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def __exit__(self, *args: Any) -> None:
|
|
52
|
+
self.close()
|
|
53
|
+
|
|
54
|
+
def request(
|
|
55
|
+
self,
|
|
56
|
+
method: str,
|
|
57
|
+
path: str,
|
|
58
|
+
*,
|
|
59
|
+
params: Optional[Dict[str, Any]] = None,
|
|
60
|
+
json: Optional[Any] = None,
|
|
61
|
+
headers: Optional[Dict[str, str]] = None,
|
|
62
|
+
response_model: Optional[Type[T]] = None,
|
|
63
|
+
) -> Union[T, Dict[str, Any], None]:
|
|
64
|
+
"""Make an HTTP request."""
|
|
65
|
+
# Filter out None values from params
|
|
66
|
+
if params:
|
|
67
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
68
|
+
|
|
69
|
+
# Merge headers
|
|
70
|
+
request_headers = {**self._headers}
|
|
71
|
+
if headers:
|
|
72
|
+
request_headers.update(headers)
|
|
73
|
+
|
|
74
|
+
response = self._client.request(
|
|
75
|
+
method=method,
|
|
76
|
+
url=path,
|
|
77
|
+
params=params,
|
|
78
|
+
json=self._serialize_body(json),
|
|
79
|
+
headers=request_headers,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return self._handle_response(response, response_model)
|
|
83
|
+
|
|
84
|
+
def upload(
|
|
85
|
+
self,
|
|
86
|
+
method: str,
|
|
87
|
+
path: str,
|
|
88
|
+
*,
|
|
89
|
+
file: Any,
|
|
90
|
+
file_name: str,
|
|
91
|
+
field_name: str = "file",
|
|
92
|
+
params: Optional[Dict[str, Any]] = None,
|
|
93
|
+
form_fields: Optional[Dict[str, str]] = None,
|
|
94
|
+
headers: Optional[Dict[str, str]] = None,
|
|
95
|
+
response_model: Optional[Type[T]] = None,
|
|
96
|
+
) -> Union[T, Dict[str, Any], None]:
|
|
97
|
+
"""Upload a file with streaming multipart/form-data."""
|
|
98
|
+
from ._upload import create_multipart_stream
|
|
99
|
+
|
|
100
|
+
content_stream, content_type = create_multipart_stream(
|
|
101
|
+
file, field_name=field_name, file_name=file_name, form_fields=form_fields,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
request_headers = {k: v for k, v in self._headers.items() if k.lower() != "content-type"}
|
|
105
|
+
request_headers["Content-Type"] = content_type
|
|
106
|
+
if headers:
|
|
107
|
+
request_headers.update(headers)
|
|
108
|
+
if params:
|
|
109
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
110
|
+
|
|
111
|
+
response = self._client.request(
|
|
112
|
+
method=method, url=path, content=content_stream, params=params, headers=request_headers,
|
|
113
|
+
)
|
|
114
|
+
return self._handle_response(response, response_model)
|
|
115
|
+
|
|
116
|
+
def _serialize_body(self, body: Any) -> Any:
|
|
117
|
+
"""Serialize request body, converting Pydantic models to dicts."""
|
|
118
|
+
if body is None:
|
|
119
|
+
return None
|
|
120
|
+
if isinstance(body, BaseModel):
|
|
121
|
+
return body.model_dump(mode="json", exclude_none=True)
|
|
122
|
+
if isinstance(body, list):
|
|
123
|
+
return [self._serialize_body(item) for item in body]
|
|
124
|
+
return body
|
|
125
|
+
|
|
126
|
+
def _handle_response(
|
|
127
|
+
self,
|
|
128
|
+
response: httpx.Response,
|
|
129
|
+
response_model: Optional[Type[T]] = None,
|
|
130
|
+
) -> Union[T, Dict[str, Any], None]:
|
|
131
|
+
"""Handle HTTP response, raising errors or parsing data."""
|
|
132
|
+
if response.status_code == 204:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
if response.status_code >= 400:
|
|
136
|
+
self._raise_error(response)
|
|
137
|
+
|
|
138
|
+
if response_model:
|
|
139
|
+
return response_model.model_validate(response.json())
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
return response.json()
|
|
143
|
+
except Exception:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def _raise_error(self, response: httpx.Response) -> None:
|
|
147
|
+
"""Raise appropriate error based on status code."""
|
|
148
|
+
try:
|
|
149
|
+
error_body = response.json()
|
|
150
|
+
except Exception:
|
|
151
|
+
error_body = {"message": response.text}
|
|
152
|
+
|
|
153
|
+
message = error_body.get("message", error_body.get("error", "Unknown error"))
|
|
154
|
+
|
|
155
|
+
if response.status_code == 401:
|
|
156
|
+
raise AuthenticationError(message, response.status_code, error_body)
|
|
157
|
+
elif response.status_code == 404:
|
|
158
|
+
raise NotFoundError(message, response.status_code, error_body)
|
|
159
|
+
elif response.status_code == 429:
|
|
160
|
+
raise RateLimitError(message, response.status_code, error_body)
|
|
161
|
+
else:
|
|
162
|
+
raise ApiError(message, response.status_code, error_body)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AsyncHttpClient:
|
|
166
|
+
"""Asynchronous HTTP client."""
|
|
167
|
+
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
base_url: str,
|
|
171
|
+
api_key: Optional[str] = None,
|
|
172
|
+
bearer_token: Optional[str] = None,
|
|
173
|
+
timeout: float = 30.0,
|
|
174
|
+
headers: Optional[Dict[str, str]] = None,
|
|
175
|
+
):
|
|
176
|
+
self.base_url = base_url.rstrip("/")
|
|
177
|
+
self._timeout = timeout
|
|
178
|
+
self._headers = headers or {}
|
|
179
|
+
|
|
180
|
+
if api_key:
|
|
181
|
+
self._headers["Meibel-API-Key"] = api_key
|
|
182
|
+
if bearer_token:
|
|
183
|
+
self._headers["Authorization"] = f"Bearer {bearer_token}"
|
|
184
|
+
|
|
185
|
+
self._client = httpx.AsyncClient(
|
|
186
|
+
base_url=self.base_url,
|
|
187
|
+
timeout=timeout,
|
|
188
|
+
headers=self._headers,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
async def close(self) -> None:
|
|
192
|
+
"""Close the HTTP client."""
|
|
193
|
+
await self._client.aclose()
|
|
194
|
+
|
|
195
|
+
async def __aenter__(self) -> "AsyncHttpClient":
|
|
196
|
+
return self
|
|
197
|
+
|
|
198
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
199
|
+
await self.close()
|
|
200
|
+
|
|
201
|
+
async def request(
|
|
202
|
+
self,
|
|
203
|
+
method: str,
|
|
204
|
+
path: str,
|
|
205
|
+
*,
|
|
206
|
+
params: Optional[Dict[str, Any]] = None,
|
|
207
|
+
json: Optional[Any] = None,
|
|
208
|
+
headers: Optional[Dict[str, str]] = None,
|
|
209
|
+
response_model: Optional[Type[T]] = None,
|
|
210
|
+
) -> Union[T, Dict[str, Any], None]:
|
|
211
|
+
"""Make an HTTP request."""
|
|
212
|
+
# Filter out None values from params
|
|
213
|
+
if params:
|
|
214
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
215
|
+
|
|
216
|
+
# Merge headers
|
|
217
|
+
request_headers = {**self._headers}
|
|
218
|
+
if headers:
|
|
219
|
+
request_headers.update(headers)
|
|
220
|
+
|
|
221
|
+
response = await self._client.request(
|
|
222
|
+
method=method,
|
|
223
|
+
url=path,
|
|
224
|
+
params=params,
|
|
225
|
+
json=self._serialize_body(json),
|
|
226
|
+
headers=request_headers,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return self._handle_response(response, response_model)
|
|
230
|
+
|
|
231
|
+
async def upload(
|
|
232
|
+
self,
|
|
233
|
+
method: str,
|
|
234
|
+
path: str,
|
|
235
|
+
*,
|
|
236
|
+
file: Any,
|
|
237
|
+
file_name: str,
|
|
238
|
+
field_name: str = "file",
|
|
239
|
+
params: Optional[Dict[str, Any]] = None,
|
|
240
|
+
form_fields: Optional[Dict[str, str]] = None,
|
|
241
|
+
headers: Optional[Dict[str, str]] = None,
|
|
242
|
+
response_model: Optional[Type[T]] = None,
|
|
243
|
+
) -> Union[T, Dict[str, Any], None]:
|
|
244
|
+
"""Upload a file with streaming multipart/form-data."""
|
|
245
|
+
from ._upload import create_async_multipart_stream
|
|
246
|
+
|
|
247
|
+
content_stream, content_type = await create_async_multipart_stream(
|
|
248
|
+
file, field_name=field_name, file_name=file_name, form_fields=form_fields,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
request_headers = {k: v for k, v in self._headers.items() if k.lower() != "content-type"}
|
|
252
|
+
request_headers["Content-Type"] = content_type
|
|
253
|
+
if headers:
|
|
254
|
+
request_headers.update(headers)
|
|
255
|
+
if params:
|
|
256
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
257
|
+
|
|
258
|
+
response = await self._client.request(
|
|
259
|
+
method=method, url=path, content=content_stream, params=params, headers=request_headers,
|
|
260
|
+
)
|
|
261
|
+
return self._handle_response(response, response_model)
|
|
262
|
+
|
|
263
|
+
def _serialize_body(self, body: Any) -> Any:
|
|
264
|
+
"""Serialize request body, converting Pydantic models to dicts."""
|
|
265
|
+
if body is None:
|
|
266
|
+
return None
|
|
267
|
+
if isinstance(body, BaseModel):
|
|
268
|
+
return body.model_dump(mode="json", exclude_none=True)
|
|
269
|
+
if isinstance(body, list):
|
|
270
|
+
return [self._serialize_body(item) for item in body]
|
|
271
|
+
return body
|
|
272
|
+
|
|
273
|
+
def _handle_response(
|
|
274
|
+
self,
|
|
275
|
+
response: httpx.Response,
|
|
276
|
+
response_model: Optional[Type[T]] = None,
|
|
277
|
+
) -> Union[T, Dict[str, Any], None]:
|
|
278
|
+
"""Handle HTTP response, raising errors or parsing data."""
|
|
279
|
+
if response.status_code == 204:
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
if response.status_code >= 400:
|
|
283
|
+
self._raise_error(response)
|
|
284
|
+
|
|
285
|
+
if response_model:
|
|
286
|
+
return response_model.model_validate(response.json())
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
return response.json()
|
|
290
|
+
except Exception:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
def _raise_error(self, response: httpx.Response) -> None:
|
|
294
|
+
"""Raise appropriate error based on status code."""
|
|
295
|
+
try:
|
|
296
|
+
error_body = response.json()
|
|
297
|
+
except Exception:
|
|
298
|
+
error_body = {"message": response.text}
|
|
299
|
+
|
|
300
|
+
message = error_body.get("message", error_body.get("error", "Unknown error"))
|
|
301
|
+
|
|
302
|
+
if response.status_code == 401:
|
|
303
|
+
raise AuthenticationError(message, response.status_code, error_body)
|
|
304
|
+
elif response.status_code == 404:
|
|
305
|
+
raise NotFoundError(message, response.status_code, error_body)
|
|
306
|
+
elif response.status_code == 429:
|
|
307
|
+
raise RateLimitError(message, response.status_code, error_body)
|
|
308
|
+
else:
|
|
309
|
+
raise ApiError(message, response.status_code, error_body)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pagination Module
|
|
3
|
+
|
|
4
|
+
Provides iterators for paginated API responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import (
|
|
10
|
+
Any,
|
|
11
|
+
AsyncIterator,
|
|
12
|
+
Callable,
|
|
13
|
+
Dict,
|
|
14
|
+
Generic,
|
|
15
|
+
Iterator,
|
|
16
|
+
List,
|
|
17
|
+
Optional,
|
|
18
|
+
TypeVar,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PaginatedIterator(Generic[T]):
|
|
25
|
+
"""
|
|
26
|
+
Iterator for paginated API responses.
|
|
27
|
+
|
|
28
|
+
Supports cursor-based, offset-based, and page-based pagination.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
fetch_page: Callable[[Optional[str]], Dict[str, Any]],
|
|
34
|
+
extract_items: Callable[[Dict[str, Any]], List[T]],
|
|
35
|
+
extract_cursor: Callable[[Dict[str, Any]], Optional[str]],
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize the paginated iterator.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
fetch_page: Function that fetches a page given a cursor (None for first page)
|
|
42
|
+
extract_items: Function that extracts items from the response
|
|
43
|
+
extract_cursor: Function that extracts the next cursor from the response
|
|
44
|
+
"""
|
|
45
|
+
self._fetch_page = fetch_page
|
|
46
|
+
self._extract_items = extract_items
|
|
47
|
+
self._extract_cursor = extract_cursor
|
|
48
|
+
self._cursor: Optional[str] = None
|
|
49
|
+
self._buffer: List[T] = []
|
|
50
|
+
self._exhausted = False
|
|
51
|
+
|
|
52
|
+
def __iter__(self) -> Iterator[T]:
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __next__(self) -> T:
|
|
56
|
+
# If we have items in the buffer, return the next one
|
|
57
|
+
if self._buffer:
|
|
58
|
+
return self._buffer.pop(0)
|
|
59
|
+
|
|
60
|
+
# If we've exhausted all pages, stop
|
|
61
|
+
if self._exhausted:
|
|
62
|
+
raise StopIteration
|
|
63
|
+
|
|
64
|
+
# Fetch the next page
|
|
65
|
+
response = self._fetch_page(self._cursor)
|
|
66
|
+
items = self._extract_items(response)
|
|
67
|
+
next_cursor = self._extract_cursor(response)
|
|
68
|
+
|
|
69
|
+
# Update state
|
|
70
|
+
if next_cursor:
|
|
71
|
+
self._cursor = next_cursor
|
|
72
|
+
else:
|
|
73
|
+
self._exhausted = True
|
|
74
|
+
|
|
75
|
+
# If no items, stop
|
|
76
|
+
if not items:
|
|
77
|
+
raise StopIteration
|
|
78
|
+
|
|
79
|
+
# Buffer items and return the first one
|
|
80
|
+
self._buffer = items[1:]
|
|
81
|
+
return items[0]
|
|
82
|
+
|
|
83
|
+
def collect(self) -> List[T]:
|
|
84
|
+
"""Collect all items into a list."""
|
|
85
|
+
return list(self)
|
|
86
|
+
|
|
87
|
+
def take(self, n: int) -> List[T]:
|
|
88
|
+
"""Take up to n items."""
|
|
89
|
+
result: List[T] = []
|
|
90
|
+
for item in self:
|
|
91
|
+
result.append(item)
|
|
92
|
+
if len(result) >= n:
|
|
93
|
+
break
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AsyncPaginatedIterator(Generic[T]):
|
|
98
|
+
"""
|
|
99
|
+
Async iterator for paginated API responses.
|
|
100
|
+
|
|
101
|
+
Supports cursor-based, offset-based, and page-based pagination.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
fetch_page: Callable[[Optional[str]], Any], # Returns Awaitable
|
|
107
|
+
extract_items: Callable[[Dict[str, Any]], List[T]],
|
|
108
|
+
extract_cursor: Callable[[Dict[str, Any]], Optional[str]],
|
|
109
|
+
):
|
|
110
|
+
"""
|
|
111
|
+
Initialize the async paginated iterator.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
fetch_page: Async function that fetches a page given a cursor (None for first page)
|
|
115
|
+
extract_items: Function that extracts items from the response
|
|
116
|
+
extract_cursor: Function that extracts the next cursor from the response
|
|
117
|
+
"""
|
|
118
|
+
self._fetch_page = fetch_page
|
|
119
|
+
self._extract_items = extract_items
|
|
120
|
+
self._extract_cursor = extract_cursor
|
|
121
|
+
self._cursor: Optional[str] = None
|
|
122
|
+
self._buffer: List[T] = []
|
|
123
|
+
self._exhausted = False
|
|
124
|
+
|
|
125
|
+
def __aiter__(self) -> AsyncIterator[T]:
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
async def __anext__(self) -> T:
|
|
129
|
+
# If we have items in the buffer, return the next one
|
|
130
|
+
if self._buffer:
|
|
131
|
+
return self._buffer.pop(0)
|
|
132
|
+
|
|
133
|
+
# If we've exhausted all pages, stop
|
|
134
|
+
if self._exhausted:
|
|
135
|
+
raise StopAsyncIteration
|
|
136
|
+
|
|
137
|
+
# Fetch the next page
|
|
138
|
+
response = await self._fetch_page(self._cursor)
|
|
139
|
+
items = self._extract_items(response)
|
|
140
|
+
next_cursor = self._extract_cursor(response)
|
|
141
|
+
|
|
142
|
+
# Update state
|
|
143
|
+
if next_cursor:
|
|
144
|
+
self._cursor = next_cursor
|
|
145
|
+
else:
|
|
146
|
+
self._exhausted = True
|
|
147
|
+
|
|
148
|
+
# If no items, stop
|
|
149
|
+
if not items:
|
|
150
|
+
raise StopAsyncIteration
|
|
151
|
+
|
|
152
|
+
# Buffer items and return the first one
|
|
153
|
+
self._buffer = items[1:]
|
|
154
|
+
return items[0]
|
|
155
|
+
|
|
156
|
+
async def collect(self) -> List[T]:
|
|
157
|
+
"""Collect all items into a list."""
|
|
158
|
+
result: List[T] = []
|
|
159
|
+
async for item in self:
|
|
160
|
+
result.append(item)
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
async def take(self, n: int) -> List[T]:
|
|
164
|
+
"""Take up to n items."""
|
|
165
|
+
result: List[T] = []
|
|
166
|
+
async for item in self:
|
|
167
|
+
result.append(item)
|
|
168
|
+
if len(result) >= n:
|
|
169
|
+
break
|
|
170
|
+
return result
|