polyembed 0.0.1__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.
- polyembed/__init__.py +19 -0
- polyembed/client.py +224 -0
- polyembed-0.0.1.dist-info/METADATA +88 -0
- polyembed-0.0.1.dist-info/RECORD +5 -0
- polyembed-0.0.1.dist-info/WHEEL +4 -0
polyembed/__init__.py
ADDED
|
@@ -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
|
+
]
|
polyembed/client.py
ADDED
|
@@ -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,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,5 @@
|
|
|
1
|
+
polyembed/__init__.py,sha256=d8elOyOozM6zQXkaF7oM-k9l1S8v4Yrxtvje2ngns60,336
|
|
2
|
+
polyembed/client.py,sha256=T7_CfR40mxwRzYKJweQdghH38WAh_Xnla98tc2fizNA,7514
|
|
3
|
+
polyembed-0.0.1.dist-info/METADATA,sha256=M4IgHyGe0qXcsfIgQiBD6_drUcUsN8iV2zD-gsdmWXk,1950
|
|
4
|
+
polyembed-0.0.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
5
|
+
polyembed-0.0.1.dist-info/RECORD,,
|