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 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any