polyembed 0.0.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.
- polyembed-0.0.1/.gitignore +23 -0
- polyembed-0.0.1/PKG-INFO +88 -0
- polyembed-0.0.1/README.md +78 -0
- polyembed-0.0.1/pyproject.toml +21 -0
- polyembed-0.0.1/src/polyembed/__init__.py +19 -0
- polyembed-0.0.1/src/polyembed/client.py +224 -0
- polyembed-0.0.1/tests/test_client.py +171 -0
- polyembed-0.0.1/uv.lock +245 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
.venv/
|
|
9
|
+
|
|
10
|
+
# Django
|
|
11
|
+
db.sqlite3
|
|
12
|
+
staticfiles/
|
|
13
|
+
.env
|
|
14
|
+
|
|
15
|
+
# IDE
|
|
16
|
+
.VSCodeCounter/
|
|
17
|
+
|
|
18
|
+
# Embedding model cache
|
|
19
|
+
models/
|
|
20
|
+
|
|
21
|
+
# Frontend
|
|
22
|
+
node_modules/
|
|
23
|
+
core/static/core/dist/
|
polyembed-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polyembed
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python SDK for the PolyEmbed embeddings API
|
|
5
|
+
Author: Brainpolo
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: httpx
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# polyembed
|
|
12
|
+
|
|
13
|
+
Python SDK for the [PolyEmbed](https://polyembed.com) embeddings API by Brainpolo.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install polyembed
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from polyembed import PolyEmbed
|
|
25
|
+
|
|
26
|
+
client = PolyEmbed(api_key="pe_<team>_<key>")
|
|
27
|
+
|
|
28
|
+
# Single embedding
|
|
29
|
+
embedding = client.embed("hello world", input_type="document")
|
|
30
|
+
# [0.0229, 0.0319, ...] (1024 floats)
|
|
31
|
+
|
|
32
|
+
# Batch (1-100 texts)
|
|
33
|
+
embeddings = client.embed_batch(
|
|
34
|
+
["text one", "text two"],
|
|
35
|
+
input_type="document",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Shielded (encrypted, non-reversible)
|
|
39
|
+
embedding = client.embed(
|
|
40
|
+
"classified report",
|
|
41
|
+
input_type="document",
|
|
42
|
+
shielded=True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Full response metadata
|
|
46
|
+
from polyembed import EmbedResponse
|
|
47
|
+
|
|
48
|
+
response = client.embed("hello", input_type="query", raw=True)
|
|
49
|
+
response.embedding # list[float]
|
|
50
|
+
response.model # "base"
|
|
51
|
+
response.shielded # False
|
|
52
|
+
response.dimensions # 1024
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
client = PolyEmbed(
|
|
59
|
+
api_key="pe_...",
|
|
60
|
+
base_url="https://polyembed.com", # default
|
|
61
|
+
timeout=30, # seconds
|
|
62
|
+
max_retries=3, # retries on 429/5xx
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Error handling
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from polyembed import AuthenticationError, ValidationError, RateLimitError, ServerError
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
embedding = client.embed("hello", input_type="document")
|
|
73
|
+
except AuthenticationError:
|
|
74
|
+
# 401 — invalid or expired API key
|
|
75
|
+
except ValidationError:
|
|
76
|
+
# 400 — invalid parameters
|
|
77
|
+
except RateLimitError:
|
|
78
|
+
# 429 — rate limit exceeded (retried automatically)
|
|
79
|
+
except ServerError:
|
|
80
|
+
# 500/502/503/504 — server error (retried automatically)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Context manager
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
with PolyEmbed(api_key="pe_...") as client:
|
|
87
|
+
embedding = client.embed("hello", input_type="document")
|
|
88
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# polyembed
|
|
2
|
+
|
|
3
|
+
Python SDK for the [PolyEmbed](https://polyembed.com) embeddings API by Brainpolo.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install polyembed
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from polyembed import PolyEmbed
|
|
15
|
+
|
|
16
|
+
client = PolyEmbed(api_key="pe_<team>_<key>")
|
|
17
|
+
|
|
18
|
+
# Single embedding
|
|
19
|
+
embedding = client.embed("hello world", input_type="document")
|
|
20
|
+
# [0.0229, 0.0319, ...] (1024 floats)
|
|
21
|
+
|
|
22
|
+
# Batch (1-100 texts)
|
|
23
|
+
embeddings = client.embed_batch(
|
|
24
|
+
["text one", "text two"],
|
|
25
|
+
input_type="document",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Shielded (encrypted, non-reversible)
|
|
29
|
+
embedding = client.embed(
|
|
30
|
+
"classified report",
|
|
31
|
+
input_type="document",
|
|
32
|
+
shielded=True,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Full response metadata
|
|
36
|
+
from polyembed import EmbedResponse
|
|
37
|
+
|
|
38
|
+
response = client.embed("hello", input_type="query", raw=True)
|
|
39
|
+
response.embedding # list[float]
|
|
40
|
+
response.model # "base"
|
|
41
|
+
response.shielded # False
|
|
42
|
+
response.dimensions # 1024
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
client = PolyEmbed(
|
|
49
|
+
api_key="pe_...",
|
|
50
|
+
base_url="https://polyembed.com", # default
|
|
51
|
+
timeout=30, # seconds
|
|
52
|
+
max_retries=3, # retries on 429/5xx
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Error handling
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from polyembed import AuthenticationError, ValidationError, RateLimitError, ServerError
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
embedding = client.embed("hello", input_type="document")
|
|
63
|
+
except AuthenticationError:
|
|
64
|
+
# 401 — invalid or expired API key
|
|
65
|
+
except ValidationError:
|
|
66
|
+
# 400 — invalid parameters
|
|
67
|
+
except RateLimitError:
|
|
68
|
+
# 429 — rate limit exceeded (retried automatically)
|
|
69
|
+
except ServerError:
|
|
70
|
+
# 500/502/503/504 — server error (retried automatically)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Context manager
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
with PolyEmbed(api_key="pe_...") as client:
|
|
77
|
+
embedding = client.embed("hello", input_type="document")
|
|
78
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "polyembed"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Python SDK for the PolyEmbed embeddings API"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Brainpolo" }]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"httpx",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[dependency-groups]
|
|
14
|
+
dev = [
|
|
15
|
+
"pytest",
|
|
16
|
+
"respx",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["hatchling"]
|
|
21
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from polyembed.client import (
|
|
2
|
+
AuthenticationError,
|
|
3
|
+
EmbedResponse,
|
|
4
|
+
PolyEmbed,
|
|
5
|
+
PolyEmbedError,
|
|
6
|
+
RateLimitError,
|
|
7
|
+
ServerError,
|
|
8
|
+
ValidationError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AuthenticationError",
|
|
13
|
+
"EmbedResponse",
|
|
14
|
+
"PolyEmbed",
|
|
15
|
+
"PolyEmbedError",
|
|
16
|
+
"RateLimitError",
|
|
17
|
+
"ServerError",
|
|
18
|
+
"ValidationError",
|
|
19
|
+
]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""PolyEmbed Python SDK — embeddings API client with retry and timeout."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PolyEmbedError(Exception):
|
|
13
|
+
"""Base exception for PolyEmbed SDK errors."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str, status_code: int | None = None):
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
super().__init__(message)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AuthenticationError(PolyEmbedError):
|
|
21
|
+
"""Invalid, revoked, or expired API key (401)."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ValidationError(PolyEmbedError):
|
|
25
|
+
"""Invalid request parameters (400)."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RateLimitError(PolyEmbedError):
|
|
29
|
+
"""Rate limit exceeded (429)."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ServerError(PolyEmbedError):
|
|
33
|
+
"""Server-side error (500/502/503/504)."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class EmbedResponse:
|
|
38
|
+
"""Full response from the embed endpoint."""
|
|
39
|
+
|
|
40
|
+
embedding: list[float]
|
|
41
|
+
model: str
|
|
42
|
+
shielded: bool
|
|
43
|
+
dimensions: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PolyEmbed:
|
|
50
|
+
"""Client for the PolyEmbed embeddings API.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
api_key: API key in the format pe_<team>_<token>.
|
|
54
|
+
base_url: Base URL of the PolyEmbed service.
|
|
55
|
+
timeout: Request timeout in seconds.
|
|
56
|
+
max_retries: Maximum number of retries on transient failures.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
api_key: str,
|
|
62
|
+
base_url: str = "https://polyembed.com",
|
|
63
|
+
timeout: float = 30,
|
|
64
|
+
max_retries: int = 3,
|
|
65
|
+
):
|
|
66
|
+
self._api_key = api_key
|
|
67
|
+
self._base_url = base_url.rstrip("/")
|
|
68
|
+
self._timeout = timeout
|
|
69
|
+
self._max_retries = max_retries
|
|
70
|
+
self._client = httpx.Client(
|
|
71
|
+
base_url=self._base_url,
|
|
72
|
+
headers={
|
|
73
|
+
"Authorization": f"Bearer {api_key}",
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
},
|
|
76
|
+
timeout=timeout,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def embed(
|
|
80
|
+
self,
|
|
81
|
+
text: str,
|
|
82
|
+
input_type: Literal["document", "query"],
|
|
83
|
+
model: Literal["base", "enhanced"] = "base",
|
|
84
|
+
shielded: bool = False,
|
|
85
|
+
raw: bool = False,
|
|
86
|
+
) -> list[float] | EmbedResponse:
|
|
87
|
+
"""Generate an embedding for a single text.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
text: Text to embed. Long texts are automatically split and averaged.
|
|
91
|
+
input_type: "document" for indexing, "query" for search.
|
|
92
|
+
model: Embedding model. "base" (default) or "enhanced".
|
|
93
|
+
shielded: Apply per-team property-preserving encryption.
|
|
94
|
+
raw: If True, return full EmbedResponse instead of just the vector.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
list[float] (1024 dimensions) or EmbedResponse if raw=True.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
AuthenticationError: Invalid or expired API key.
|
|
101
|
+
ValidationError: Invalid request parameters.
|
|
102
|
+
RateLimitError: Rate limit exceeded.
|
|
103
|
+
ServerError: Server-side error after all retries exhausted.
|
|
104
|
+
"""
|
|
105
|
+
data = self._request(
|
|
106
|
+
"/api/v1/embed/",
|
|
107
|
+
{"text": text, "input_type": input_type, "model": model, "shielded": shielded},
|
|
108
|
+
)
|
|
109
|
+
if raw:
|
|
110
|
+
return EmbedResponse(
|
|
111
|
+
embedding=data["embedding"],
|
|
112
|
+
model=data["model"],
|
|
113
|
+
shielded=data["shielded"],
|
|
114
|
+
dimensions=data["dimensions"],
|
|
115
|
+
)
|
|
116
|
+
return data["embedding"]
|
|
117
|
+
|
|
118
|
+
def embed_batch(
|
|
119
|
+
self,
|
|
120
|
+
texts: list[str],
|
|
121
|
+
input_type: Literal["document", "query"],
|
|
122
|
+
model: Literal["base", "enhanced"] = "base",
|
|
123
|
+
shielded: bool = False,
|
|
124
|
+
) -> list[list[float]]:
|
|
125
|
+
"""Generate embeddings for multiple texts in one request.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
texts: List of texts to embed (1-100 items).
|
|
129
|
+
input_type: "document" for indexing, "query" for search.
|
|
130
|
+
model: Embedding model. "base" (default) or "enhanced".
|
|
131
|
+
shielded: Apply per-team property-preserving encryption.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of embeddings in the same order as input texts.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
AuthenticationError: Invalid or expired API key.
|
|
138
|
+
ValidationError: Invalid request parameters or batch too large.
|
|
139
|
+
RateLimitError: Rate limit exceeded.
|
|
140
|
+
ServerError: Server-side error after all retries exhausted.
|
|
141
|
+
"""
|
|
142
|
+
data = self._request(
|
|
143
|
+
"/api/v1/embed/batch/",
|
|
144
|
+
{"texts": texts, "input_type": input_type, "model": model, "shielded": shielded},
|
|
145
|
+
)
|
|
146
|
+
return data["embeddings"]
|
|
147
|
+
|
|
148
|
+
def _request(self, path: str, payload: dict) -> dict:
|
|
149
|
+
"""Make a POST request with retry logic."""
|
|
150
|
+
last_error: Exception | None = None
|
|
151
|
+
|
|
152
|
+
for attempt in range(self._max_retries + 1):
|
|
153
|
+
try:
|
|
154
|
+
response = self._client.post(path, json=payload)
|
|
155
|
+
except httpx.TimeoutException as e:
|
|
156
|
+
last_error = ServerError(f"Request timed out: {e}", status_code=None)
|
|
157
|
+
if attempt < self._max_retries:
|
|
158
|
+
time.sleep(self._backoff(attempt))
|
|
159
|
+
continue
|
|
160
|
+
raise last_error from e
|
|
161
|
+
except httpx.HTTPError as e:
|
|
162
|
+
last_error = ServerError(f"Connection error: {e}", status_code=None)
|
|
163
|
+
if attempt < self._max_retries:
|
|
164
|
+
time.sleep(self._backoff(attempt))
|
|
165
|
+
continue
|
|
166
|
+
raise last_error from e
|
|
167
|
+
|
|
168
|
+
if response.status_code == 200:
|
|
169
|
+
return response.json()
|
|
170
|
+
|
|
171
|
+
# Non-retryable errors — raise immediately
|
|
172
|
+
if response.status_code == 400:
|
|
173
|
+
raise ValidationError(self._detail(response), status_code=400)
|
|
174
|
+
if response.status_code == 401:
|
|
175
|
+
raise AuthenticationError(self._detail(response), status_code=401)
|
|
176
|
+
|
|
177
|
+
# Retryable errors
|
|
178
|
+
if response.status_code in RETRYABLE_STATUS_CODES:
|
|
179
|
+
last_error = (
|
|
180
|
+
RateLimitError(self._detail(response), status_code=429)
|
|
181
|
+
if response.status_code == 429
|
|
182
|
+
else ServerError(self._detail(response), status_code=response.status_code)
|
|
183
|
+
)
|
|
184
|
+
if attempt < self._max_retries:
|
|
185
|
+
wait = self._retry_after(response) or self._backoff(attempt)
|
|
186
|
+
time.sleep(wait)
|
|
187
|
+
continue
|
|
188
|
+
raise last_error
|
|
189
|
+
|
|
190
|
+
# Unknown status code — raise as server error, no retry
|
|
191
|
+
raise ServerError(self._detail(response), status_code=response.status_code)
|
|
192
|
+
|
|
193
|
+
raise last_error or ServerError("Request failed after all retries")
|
|
194
|
+
|
|
195
|
+
def _backoff(self, attempt: int) -> float:
|
|
196
|
+
"""Exponential backoff: 0.5s, 1s, 2s, ..."""
|
|
197
|
+
return 0.5 * (2**attempt)
|
|
198
|
+
|
|
199
|
+
def _retry_after(self, response: httpx.Response) -> float | None:
|
|
200
|
+
"""Parse Retry-After header if present."""
|
|
201
|
+
value = response.headers.get("Retry-After")
|
|
202
|
+
if value is None:
|
|
203
|
+
return None
|
|
204
|
+
try:
|
|
205
|
+
return float(value)
|
|
206
|
+
except ValueError:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def _detail(self, response: httpx.Response) -> str:
|
|
210
|
+
"""Extract error detail from response."""
|
|
211
|
+
try:
|
|
212
|
+
return response.json().get("detail", response.text)
|
|
213
|
+
except Exception:
|
|
214
|
+
return response.text
|
|
215
|
+
|
|
216
|
+
def close(self) -> None:
|
|
217
|
+
"""Close the underlying HTTP client."""
|
|
218
|
+
self._client.close()
|
|
219
|
+
|
|
220
|
+
def __enter__(self):
|
|
221
|
+
return self
|
|
222
|
+
|
|
223
|
+
def __exit__(self, *args):
|
|
224
|
+
self.close()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import pytest
|
|
3
|
+
import respx
|
|
4
|
+
|
|
5
|
+
from polyembed import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
EmbedResponse,
|
|
8
|
+
PolyEmbed,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
ServerError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
BASE_URL = "https://polyembed.com"
|
|
15
|
+
MOCK_EMBEDDING = [0.1] * 1024
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def client():
|
|
20
|
+
c = PolyEmbed(api_key="pe_test_key", base_url=BASE_URL, max_retries=2)
|
|
21
|
+
yield c
|
|
22
|
+
c.close()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# --- embed ---
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@respx.mock
|
|
29
|
+
def test_embed_returns_vector(client):
|
|
30
|
+
respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
31
|
+
return_value=httpx.Response(200, json={"embedding": MOCK_EMBEDDING, "model": "base", "shielded": False, "dimensions": 1024})
|
|
32
|
+
)
|
|
33
|
+
result = client.embed("hello", input_type="document")
|
|
34
|
+
assert isinstance(result, list)
|
|
35
|
+
assert len(result) == 1024
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@respx.mock
|
|
39
|
+
def test_embed_raw_returns_response_object(client):
|
|
40
|
+
respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
41
|
+
return_value=httpx.Response(200, json={"embedding": MOCK_EMBEDDING, "model": "base", "shielded": True, "dimensions": 1024})
|
|
42
|
+
)
|
|
43
|
+
result = client.embed("hello", input_type="query", shielded=True, raw=True)
|
|
44
|
+
assert isinstance(result, EmbedResponse)
|
|
45
|
+
assert result.shielded is True
|
|
46
|
+
assert result.dimensions == 1024
|
|
47
|
+
assert result.model == "base"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@respx.mock
|
|
51
|
+
def test_embed_sends_correct_payload(client):
|
|
52
|
+
route = respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
53
|
+
return_value=httpx.Response(200, json={"embedding": MOCK_EMBEDDING, "model": "base", "shielded": False, "dimensions": 1024})
|
|
54
|
+
)
|
|
55
|
+
client.embed("test text", input_type="document", model="base", shielded=True)
|
|
56
|
+
payload = route.calls[0].request.content
|
|
57
|
+
import json
|
|
58
|
+
|
|
59
|
+
body = json.loads(payload)
|
|
60
|
+
assert body["text"] == "test text"
|
|
61
|
+
assert body["input_type"] == "document"
|
|
62
|
+
assert body["model"] == "base"
|
|
63
|
+
assert body["shielded"] is True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@respx.mock
|
|
67
|
+
def test_embed_sends_auth_header(client):
|
|
68
|
+
route = respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
69
|
+
return_value=httpx.Response(200, json={"embedding": MOCK_EMBEDDING, "model": "base", "shielded": False, "dimensions": 1024})
|
|
70
|
+
)
|
|
71
|
+
client.embed("hello", input_type="document")
|
|
72
|
+
assert route.calls[0].request.headers["authorization"] == "Bearer pe_test_key"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --- embed_batch ---
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@respx.mock
|
|
79
|
+
def test_embed_batch_returns_list(client):
|
|
80
|
+
respx.post(f"{BASE_URL}/api/v1/embed/batch/").mock(
|
|
81
|
+
return_value=httpx.Response(200, json={"embeddings": [MOCK_EMBEDDING, MOCK_EMBEDDING], "count": 2, "model": "base", "shielded": False, "dimensions": 1024})
|
|
82
|
+
)
|
|
83
|
+
result = client.embed_batch(["a", "b"], input_type="document")
|
|
84
|
+
assert len(result) == 2
|
|
85
|
+
assert len(result[0]) == 1024
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --- Error handling ---
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@respx.mock
|
|
92
|
+
def test_401_raises_authentication_error(client):
|
|
93
|
+
respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
94
|
+
return_value=httpx.Response(401, json={"detail": "Invalid API key."})
|
|
95
|
+
)
|
|
96
|
+
with pytest.raises(AuthenticationError, match="Invalid API key"):
|
|
97
|
+
client.embed("hello", input_type="document")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@respx.mock
|
|
101
|
+
def test_400_raises_validation_error(client):
|
|
102
|
+
respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
103
|
+
return_value=httpx.Response(400, json={"detail": "Missing field."})
|
|
104
|
+
)
|
|
105
|
+
with pytest.raises(ValidationError, match="Missing field"):
|
|
106
|
+
client.embed("hello", input_type="document")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@respx.mock
|
|
110
|
+
def test_429_raises_rate_limit_error(client):
|
|
111
|
+
respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
112
|
+
return_value=httpx.Response(429, json={"detail": "Rate limited."})
|
|
113
|
+
)
|
|
114
|
+
with pytest.raises(RateLimitError):
|
|
115
|
+
client.embed("hello", input_type="document")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@respx.mock
|
|
119
|
+
def test_500_raises_server_error(client):
|
|
120
|
+
respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
121
|
+
return_value=httpx.Response(500, json={"detail": "Internal error."})
|
|
122
|
+
)
|
|
123
|
+
with pytest.raises(ServerError):
|
|
124
|
+
client.embed("hello", input_type="document")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --- Retry ---
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@respx.mock
|
|
131
|
+
def test_retries_on_503_then_succeeds(client):
|
|
132
|
+
route = respx.post(f"{BASE_URL}/api/v1/embed/")
|
|
133
|
+
route.side_effect = [
|
|
134
|
+
httpx.Response(503, json={"detail": "Unavailable"}),
|
|
135
|
+
httpx.Response(200, json={"embedding": MOCK_EMBEDDING, "model": "base", "shielded": False, "dimensions": 1024}),
|
|
136
|
+
]
|
|
137
|
+
result = client.embed("hello", input_type="document")
|
|
138
|
+
assert len(result) == 1024
|
|
139
|
+
assert route.call_count == 2
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@respx.mock
|
|
143
|
+
def test_no_retry_on_401(client):
|
|
144
|
+
route = respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
145
|
+
return_value=httpx.Response(401, json={"detail": "Bad key."})
|
|
146
|
+
)
|
|
147
|
+
with pytest.raises(AuthenticationError):
|
|
148
|
+
client.embed("hello", input_type="document")
|
|
149
|
+
assert route.call_count == 1
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@respx.mock
|
|
153
|
+
def test_exhausts_retries_then_raises(client):
|
|
154
|
+
respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
155
|
+
return_value=httpx.Response(503, json={"detail": "Down"})
|
|
156
|
+
)
|
|
157
|
+
with pytest.raises(ServerError):
|
|
158
|
+
client.embed("hello", input_type="document")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --- Context manager ---
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@respx.mock
|
|
165
|
+
def test_context_manager():
|
|
166
|
+
respx.post(f"{BASE_URL}/api/v1/embed/").mock(
|
|
167
|
+
return_value=httpx.Response(200, json={"embedding": MOCK_EMBEDDING, "model": "base", "shielded": False, "dimensions": 1024})
|
|
168
|
+
)
|
|
169
|
+
with PolyEmbed(api_key="pe_test_key") as client:
|
|
170
|
+
result = client.embed("hello", input_type="document")
|
|
171
|
+
assert len(result) == 1024
|
polyembed-0.0.1/uv.lock
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
version = 1
|
|
2
|
+
revision = 3
|
|
3
|
+
requires-python = ">=3.10"
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "anyio"
|
|
7
|
+
version = "4.12.1"
|
|
8
|
+
source = { registry = "https://pypi.org/simple" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
11
|
+
{ name = "idna" },
|
|
12
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
13
|
+
]
|
|
14
|
+
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
|
|
15
|
+
wheels = [
|
|
16
|
+
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[[package]]
|
|
20
|
+
name = "certifi"
|
|
21
|
+
version = "2026.2.25"
|
|
22
|
+
source = { registry = "https://pypi.org/simple" }
|
|
23
|
+
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
|
24
|
+
wheels = [
|
|
25
|
+
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[[package]]
|
|
29
|
+
name = "colorama"
|
|
30
|
+
version = "0.4.6"
|
|
31
|
+
source = { registry = "https://pypi.org/simple" }
|
|
32
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
|
33
|
+
wheels = [
|
|
34
|
+
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[[package]]
|
|
38
|
+
name = "exceptiongroup"
|
|
39
|
+
version = "1.3.1"
|
|
40
|
+
source = { registry = "https://pypi.org/simple" }
|
|
41
|
+
dependencies = [
|
|
42
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
43
|
+
]
|
|
44
|
+
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
45
|
+
wheels = [
|
|
46
|
+
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[[package]]
|
|
50
|
+
name = "h11"
|
|
51
|
+
version = "0.16.0"
|
|
52
|
+
source = { registry = "https://pypi.org/simple" }
|
|
53
|
+
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
|
54
|
+
wheels = [
|
|
55
|
+
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[[package]]
|
|
59
|
+
name = "httpcore"
|
|
60
|
+
version = "1.0.9"
|
|
61
|
+
source = { registry = "https://pypi.org/simple" }
|
|
62
|
+
dependencies = [
|
|
63
|
+
{ name = "certifi" },
|
|
64
|
+
{ name = "h11" },
|
|
65
|
+
]
|
|
66
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
|
67
|
+
wheels = [
|
|
68
|
+
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[[package]]
|
|
72
|
+
name = "httpx"
|
|
73
|
+
version = "0.28.1"
|
|
74
|
+
source = { registry = "https://pypi.org/simple" }
|
|
75
|
+
dependencies = [
|
|
76
|
+
{ name = "anyio" },
|
|
77
|
+
{ name = "certifi" },
|
|
78
|
+
{ name = "httpcore" },
|
|
79
|
+
{ name = "idna" },
|
|
80
|
+
]
|
|
81
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
|
82
|
+
wheels = [
|
|
83
|
+
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
[[package]]
|
|
87
|
+
name = "idna"
|
|
88
|
+
version = "3.11"
|
|
89
|
+
source = { registry = "https://pypi.org/simple" }
|
|
90
|
+
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
|
91
|
+
wheels = [
|
|
92
|
+
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[[package]]
|
|
96
|
+
name = "iniconfig"
|
|
97
|
+
version = "2.3.0"
|
|
98
|
+
source = { registry = "https://pypi.org/simple" }
|
|
99
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
100
|
+
wheels = [
|
|
101
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
[[package]]
|
|
105
|
+
name = "packaging"
|
|
106
|
+
version = "26.0"
|
|
107
|
+
source = { registry = "https://pypi.org/simple" }
|
|
108
|
+
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
|
109
|
+
wheels = [
|
|
110
|
+
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
[[package]]
|
|
114
|
+
name = "pluggy"
|
|
115
|
+
version = "1.6.0"
|
|
116
|
+
source = { registry = "https://pypi.org/simple" }
|
|
117
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
118
|
+
wheels = [
|
|
119
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
[[package]]
|
|
123
|
+
name = "polyembed"
|
|
124
|
+
version = "0.0.1"
|
|
125
|
+
source = { editable = "." }
|
|
126
|
+
dependencies = [
|
|
127
|
+
{ name = "httpx" },
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
[package.dev-dependencies]
|
|
131
|
+
dev = [
|
|
132
|
+
{ name = "pytest" },
|
|
133
|
+
{ name = "respx" },
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
[package.metadata]
|
|
137
|
+
requires-dist = [{ name = "httpx" }]
|
|
138
|
+
|
|
139
|
+
[package.metadata.requires-dev]
|
|
140
|
+
dev = [
|
|
141
|
+
{ name = "pytest" },
|
|
142
|
+
{ name = "respx" },
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
[[package]]
|
|
146
|
+
name = "pygments"
|
|
147
|
+
version = "2.19.2"
|
|
148
|
+
source = { registry = "https://pypi.org/simple" }
|
|
149
|
+
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
|
150
|
+
wheels = [
|
|
151
|
+
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
[[package]]
|
|
155
|
+
name = "pytest"
|
|
156
|
+
version = "9.0.2"
|
|
157
|
+
source = { registry = "https://pypi.org/simple" }
|
|
158
|
+
dependencies = [
|
|
159
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
160
|
+
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
161
|
+
{ name = "iniconfig" },
|
|
162
|
+
{ name = "packaging" },
|
|
163
|
+
{ name = "pluggy" },
|
|
164
|
+
{ name = "pygments" },
|
|
165
|
+
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
|
166
|
+
]
|
|
167
|
+
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
|
168
|
+
wheels = [
|
|
169
|
+
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
[[package]]
|
|
173
|
+
name = "respx"
|
|
174
|
+
version = "0.22.0"
|
|
175
|
+
source = { registry = "https://pypi.org/simple" }
|
|
176
|
+
dependencies = [
|
|
177
|
+
{ name = "httpx" },
|
|
178
|
+
]
|
|
179
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" }
|
|
180
|
+
wheels = [
|
|
181
|
+
{ url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" },
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
[[package]]
|
|
185
|
+
name = "tomli"
|
|
186
|
+
version = "2.4.0"
|
|
187
|
+
source = { registry = "https://pypi.org/simple" }
|
|
188
|
+
sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
|
|
189
|
+
wheels = [
|
|
190
|
+
{ url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
|
|
191
|
+
{ url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
|
|
192
|
+
{ url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
|
|
193
|
+
{ url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
|
|
194
|
+
{ url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
|
|
195
|
+
{ url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
|
|
196
|
+
{ url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
|
|
197
|
+
{ url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
|
|
198
|
+
{ url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
|
|
199
|
+
{ url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
|
|
200
|
+
{ url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
|
|
201
|
+
{ url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
|
|
202
|
+
{ url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
|
|
203
|
+
{ url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
|
|
204
|
+
{ url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
|
|
205
|
+
{ url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
|
|
206
|
+
{ url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
|
|
207
|
+
{ url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
|
|
208
|
+
{ url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
|
|
209
|
+
{ url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
|
|
210
|
+
{ url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
|
|
211
|
+
{ url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
|
|
212
|
+
{ url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
|
|
213
|
+
{ url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
|
|
214
|
+
{ url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
|
|
215
|
+
{ url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
|
|
216
|
+
{ url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
|
|
217
|
+
{ url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
|
|
218
|
+
{ url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
|
|
219
|
+
{ url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
|
|
220
|
+
{ url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
|
|
221
|
+
{ url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
|
|
222
|
+
{ url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
|
|
223
|
+
{ url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
|
|
224
|
+
{ url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
|
|
225
|
+
{ url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
|
|
226
|
+
{ url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
|
|
227
|
+
{ url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
|
|
228
|
+
{ url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
|
|
229
|
+
{ url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
|
|
230
|
+
{ url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
|
|
231
|
+
{ url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
|
|
232
|
+
{ url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
|
|
233
|
+
{ url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
|
|
234
|
+
{ url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
|
|
235
|
+
{ url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
[[package]]
|
|
239
|
+
name = "typing-extensions"
|
|
240
|
+
version = "4.15.0"
|
|
241
|
+
source = { registry = "https://pypi.org/simple" }
|
|
242
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
|
243
|
+
wheels = [
|
|
244
|
+
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
|
245
|
+
]
|