unique-search-proxy-sdk 2026.26.0.dev4__tar.gz → 2026.26.0.dev5__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.
- unique_search_proxy_sdk-2026.26.0.dev5/PKG-INFO +221 -0
- unique_search_proxy_sdk-2026.26.0.dev5/README.md +208 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/pyproject.toml +8 -2
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/__init__.py +2 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_endpoint.py +99 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/api/agent_search/agent_search_stream_v1_agent_search_stream_post.py +168 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/api/agent_search/agent_search_v1_agent_search_post.py +170 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/api/crawl/crawl_v1_crawl_post.py +36 -11
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/api/search/__init__.py +1 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/api/search/search_v1_search_post.py +18 -11
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/__init__.py +83 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/agent_search_response.py +93 -0
- unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/models/basic_proxy_crawler.py → unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/basic_crawl_request.py +14 -37
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/bing_agent_search_request.py +137 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/brave_search_request.py +356 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/brave_search_request_safesearch_type_0.py +10 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/brave_search_request_units_type_0.py +9 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/crawl_response.py +6 -6
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/crawl_url_result.py +2 -1
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/firecrawl_crawl_request.py +302 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/firecrawl_crawl_request_proxy_mode.py +10 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/firecrawl_crawl_request_scrape_headers_type_0.py +47 -0
- unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/models/google_request.py → unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/google_search_request.py +24 -20
- unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/models/google_request_safe_search.py → unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/google_search_request_safe_search.py +1 -1
- unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/models/google_request_site_search_filter_type_0.py → unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/google_search_request_site_search_filter_type_0.py +1 -1
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/jina_crawl_request.py +380 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/jina_crawl_request_engine.py +11 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/jina_crawl_request_retain_images_type_0.py +12 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/jina_crawl_request_return_format.py +12 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/perplexity_search_request.py +417 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/perplexity_search_request_search_context_size_type_0.py +10 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/perplexity_search_request_search_recency_filter_type_0.py +12 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/providers_list_response.py +13 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/tavily_crawl_request.py +179 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/tavily_crawl_request_extract_depth.py +9 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/tavily_crawl_request_output_format.py +9 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/models/vertex_ai_agent_search_request.py +127 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_transport.py +6 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_typed_endpoints.py +419 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/agent_search_client.py +120 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/client.py +4 -2
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/converters.py +70 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/crawl_client.py +105 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/errors.py +24 -5
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/search_client.py +83 -0
- unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/typing_checks.py +57 -0
- unique_search_proxy_sdk-2026.26.0.dev4/PKG-INFO +0 -25
- unique_search_proxy_sdk-2026.26.0.dev4/README.md +0 -12
- unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/models/__init__.py +0 -37
- unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/converters.py +0 -45
- unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/crawl_client.py +0 -74
- unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/search_client.py +0 -75
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/__init__.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/api/__init__.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/api/configuration → unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/api/agent_search}/__init__.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/api/crawl → unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/api/configuration}/__init__.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/api/configuration/list_providers_v1_configuration_providers_get.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/api/health → unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/api/crawl}/__init__.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4/unique_search_proxy_sdk/_generated/api/search → unique_search_proxy_sdk-2026.26.0.dev5/unique_search_proxy_sdk/_generated/api/health}/__init__.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/api/health/health_health_get.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/api/health/ready_ready_get.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/client.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/errors.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/content_types.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/health_health_get_response_health_health_get.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/http_validation_error.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/per_url_error.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/ready_ready_get_response_ready_ready_get.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/search_response.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/validation_error.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/models/web_search_result.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_generated/types.py +0 -0
- {unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/unique_search_proxy_sdk/_http.py +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: unique-search-proxy-sdk
|
|
3
|
+
Version: 2026.26.0.dev5
|
|
4
|
+
Summary: Async HTTP client for the Unique Search Proxy API
|
|
5
|
+
Author: ThePhilAz
|
|
6
|
+
Author-email: ThePhilAz <rami.azouz@philico.com>
|
|
7
|
+
Requires-Dist: attrs>=24.3.0,<26
|
|
8
|
+
Requires-Dist: httpx>=0.28.0,<0.29.0
|
|
9
|
+
Requires-Dist: pydantic>=2.12.5,<3.0.0
|
|
10
|
+
Requires-Dist: unique-search-proxy-core>=2026.26.0.dev5,<2026.26.0rc0
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# unique-search-proxy-sdk
|
|
15
|
+
|
|
16
|
+
Part of [Unique Search Proxy](../README.md) · PyPI: `unique-search-proxy-sdk`
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. What this package is
|
|
21
|
+
|
|
22
|
+
**The SDK is the caller's HTTP client.** Platform services (e.g. assistants-core) use it to reach the proxy pod at runtime — search, agent search, and crawl — without hand-crafting httpx calls or duplicating route paths.
|
|
23
|
+
|
|
24
|
+
It depends on [core](../unique_search_proxy_core/README.md) for request validation and typed errors. It does **not** include the FastAPI server.
|
|
25
|
+
|
|
26
|
+
| Package | Question it answers |
|
|
27
|
+
|---------|---------------------|
|
|
28
|
+
| [Core](../unique_search_proxy_core/README.md) | *What* can be configured and *what* does a valid request/response look like? |
|
|
29
|
+
| [Client](../unique_search_proxy_client/README.md) | *How* are provider calls executed at runtime? |
|
|
30
|
+
| **SDK** (this) | *How* do callers reach the proxy over HTTP? |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2. Role in the system
|
|
35
|
+
|
|
36
|
+
The SDK implements **Path B** (runtime HTTP) from the [system overview](../README.md). Schema work (Path A) stays in core — the SDK does not expose deployment config or LLM call-schema endpoints.
|
|
37
|
+
|
|
38
|
+
```mermaid
|
|
39
|
+
flowchart LR
|
|
40
|
+
Caller["Caller service"]
|
|
41
|
+
SDK["unique_search_proxy_sdk"]
|
|
42
|
+
Core["unique_search_proxy_core"]
|
|
43
|
+
Proxy["unique_search_proxy_client"]
|
|
44
|
+
|
|
45
|
+
Caller --> SDK
|
|
46
|
+
SDK --> Core
|
|
47
|
+
SDK -->|"httpx"| Proxy
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Before a search call, the caller (or SDK) validates the payload against core request models. Non-2xx responses become the same `ProxyError` subclasses defined in core.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 3. Architecture
|
|
55
|
+
|
|
56
|
+
Two layers: a **hand-written facade** over **OpenAPI-generated code**.
|
|
57
|
+
|
|
58
|
+
```mermaid
|
|
59
|
+
flowchart TB
|
|
60
|
+
subgraph sdk_pkg["unique_search_proxy_sdk"]
|
|
61
|
+
Facade["UniqueSearchProxyClient"]
|
|
62
|
+
SC["SearchClient"]
|
|
63
|
+
ASC["AgentSearchClient"]
|
|
64
|
+
CC["CrawlClient"]
|
|
65
|
+
Transport["OpenapiTransport"]
|
|
66
|
+
Gen["_generated/"]
|
|
67
|
+
Conv["converters.py"]
|
|
68
|
+
Err["errors.py"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Core["unique_search_proxy_core"]
|
|
72
|
+
Proxy["Proxy pod"]
|
|
73
|
+
|
|
74
|
+
Facade --> SC & ASC & CC
|
|
75
|
+
SC & ASC & CC --> Transport
|
|
76
|
+
Transport --> Gen
|
|
77
|
+
SC --> Conv --> Core
|
|
78
|
+
Err --> Core
|
|
79
|
+
Gen -->|"httpx AsyncClient"| Proxy
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Layer | Path | Responsibility |
|
|
83
|
+
|-------|------|----------------|
|
|
84
|
+
| **Facade** | `client.py` | `UniqueSearchProxyClient` — composes sub-clients, `health()`, `ready()` |
|
|
85
|
+
| **Sub-clients** | `search_client.py`, `agent_search_client.py`, `crawl_client.py` | Per-provider typed endpoints + compatibility dispatchers |
|
|
86
|
+
| **Endpoint factory** | `_endpoint.py` | `async_post_endpoint` / `async_sse_endpoint` (ParamSpec from core request models) |
|
|
87
|
+
| **Transport** | `_transport.py`, `_http.py` | Shared httpx lifecycle, base URL, timeout |
|
|
88
|
+
| **Generated** | `_generated/` | Route functions + attrs models from OpenAPI (**do not edit**) |
|
|
89
|
+
| **Converters** | `converters.py` | Core Pydantic models → generated SDK models |
|
|
90
|
+
| **Errors** | `errors.py` | Error envelope → core `ProxyError` subclasses |
|
|
91
|
+
|
|
92
|
+
The client package is the **source of truth** for the HTTP contract. When routes change, regenerate `_generated/` from `unique_search_proxy_client/openapi.json`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 4. Usage
|
|
97
|
+
|
|
98
|
+
Each provider exposes a typed endpoint whose kwargs mirror the core `*Request` Pydantic model (via `ParamSpec`). Snake_case and camelCase aliases both work (`fetch_size` / `fetchSize`).
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from unique_search_proxy_sdk import UniqueSearchProxyClient
|
|
102
|
+
|
|
103
|
+
async with UniqueSearchProxyClient("http://unique-search-proxy:2349") as client:
|
|
104
|
+
await client.health()
|
|
105
|
+
|
|
106
|
+
# Typed per-provider endpoints (preferred)
|
|
107
|
+
result = await client.search.google(query="unique ag", gl="ch", fetch_size=10)
|
|
108
|
+
brave = await client.search.brave(query="news", country="US", safesearch="strict")
|
|
109
|
+
agent = await client.agent_search.bing(query="EU AI Act timeline", fetch_size=5)
|
|
110
|
+
crawl = await client.crawl.basic(urls=["https://example.com"])
|
|
111
|
+
|
|
112
|
+
# Compatibility dispatchers (engine/crawler string)
|
|
113
|
+
result = await client.search.search("unique ag", engine="google", fetch_size=10)
|
|
114
|
+
agent = await client.agent_search.search("query", engine="bing")
|
|
115
|
+
crawl = await client.crawl.crawl(["https://example.com"], crawler="Basic")
|
|
116
|
+
|
|
117
|
+
# Agent SSE streaming
|
|
118
|
+
async for event in client.agent_search.bing_stream(query="query"):
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
# Low-level: one generated function per route
|
|
122
|
+
raw_client = client.openapi
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Search payloads are validated through core's `parse_search_request()` before serialization. Agent and crawl clients follow the same pattern.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 5. API mapping
|
|
130
|
+
|
|
131
|
+
| Facade method | HTTP |
|
|
132
|
+
|---------------|------|
|
|
133
|
+
| `health()` | `GET /health` |
|
|
134
|
+
| `ready()` | `GET /ready` |
|
|
135
|
+
| `search.google(...)` / `.brave(...)` / `.perplexity(...)` | `POST /v1/search` |
|
|
136
|
+
| `search.search(...)` | `POST /v1/search` (dispatcher) |
|
|
137
|
+
| `agent_search.bing(...)` / `.vertexai(...)` | `POST /v1/agent-search` |
|
|
138
|
+
| `agent_search.bing_stream(...)` / `.vertexai_stream(...)` | `POST /v1/agent-search/stream` (SSE) |
|
|
139
|
+
| `agent_search.search(...)` / `.stream(...)` | same routes (dispatchers) |
|
|
140
|
+
| `crawl.basic(...)` / `.tavily(...)` / `.jina(...)` / `.firecrawl(...)` | `POST /v1/crawl` |
|
|
141
|
+
| `crawl.crawl(...)` | `POST /v1/crawl` (dispatcher) |
|
|
142
|
+
| `openapi` | Low-level generated client (one function per route) |
|
|
143
|
+
|
|
144
|
+
Endpoint payloads and provider ids → [Client README](../unique_search_proxy_client/README.md).
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 6. Error handling
|
|
149
|
+
|
|
150
|
+
Non-success responses with a proxy error envelope raise typed exceptions from core:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from unique_search_proxy_core import EngineNotConfiguredError, UpstreamError
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`ENGINE_NOT_CONFIGURED` (503) includes `missing_env_vars` parsed from the error `details` list — useful for operators and LLM tool consumers.
|
|
157
|
+
|
|
158
|
+
Transport failures (non-JSON body, connection errors) raise `UniqueSearchProxyClientError`.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 7. Codegen workflow
|
|
163
|
+
|
|
164
|
+
Generated via [openapi-python-client](https://github.com/openapi-generators/openapi-python-client) from the server's OpenAPI spec.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
cd ../unique_search_proxy_client
|
|
168
|
+
uv sync
|
|
169
|
+
uv run python scripts/generate_sdk.py
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
| Path | Role |
|
|
173
|
+
|------|------|
|
|
174
|
+
| `unique_search_proxy_sdk/_generated/` | Regenerated httpx client + attrs models |
|
|
175
|
+
| `unique_search_proxy_client/openapi.json` | Exported spec (codegen input) |
|
|
176
|
+
| `unique_search_proxy_sdk/client.py` | Hand-written facade (not regenerated) |
|
|
177
|
+
|
|
178
|
+
Other codegen tools considered: OpenAPI Generator, datamodel-code-generator, Kiota — see git history in the client README if needed.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 8. Testing
|
|
183
|
+
|
|
184
|
+
Pass an injected `httpx.AsyncClient` with `ASGITransport` for in-process tests against `create_app()`:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
import httpx
|
|
188
|
+
from httpx import ASGITransport
|
|
189
|
+
from unique_search_proxy_client.web.app import create_app
|
|
190
|
+
from unique_search_proxy_sdk import UniqueSearchProxyClient
|
|
191
|
+
|
|
192
|
+
app = create_app()
|
|
193
|
+
async with httpx.AsyncClient(
|
|
194
|
+
transport=ASGITransport(app=app),
|
|
195
|
+
base_url="http://test",
|
|
196
|
+
) as http:
|
|
197
|
+
async with UniqueSearchProxyClient("http://test", http_client=http) as client:
|
|
198
|
+
...
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Run the app lifespan so the in-process `HttpClientPool` is initialised.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 9. Installation & development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
cd unique_search_proxy_sdk
|
|
209
|
+
uv sync
|
|
210
|
+
uv run pytest
|
|
211
|
+
uv run ruff check .
|
|
212
|
+
uv run basedpyright
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Depends on `unique-search-proxy-core`. Does not depend on `unique-search-proxy` at runtime (only as a dev dependency for integration tests).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## License
|
|
220
|
+
|
|
221
|
+
Proprietary — Unique AG
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# unique-search-proxy-sdk
|
|
2
|
+
|
|
3
|
+
Part of [Unique Search Proxy](../README.md) · PyPI: `unique-search-proxy-sdk`
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. What this package is
|
|
8
|
+
|
|
9
|
+
**The SDK is the caller's HTTP client.** Platform services (e.g. assistants-core) use it to reach the proxy pod at runtime — search, agent search, and crawl — without hand-crafting httpx calls or duplicating route paths.
|
|
10
|
+
|
|
11
|
+
It depends on [core](../unique_search_proxy_core/README.md) for request validation and typed errors. It does **not** include the FastAPI server.
|
|
12
|
+
|
|
13
|
+
| Package | Question it answers |
|
|
14
|
+
|---------|---------------------|
|
|
15
|
+
| [Core](../unique_search_proxy_core/README.md) | *What* can be configured and *what* does a valid request/response look like? |
|
|
16
|
+
| [Client](../unique_search_proxy_client/README.md) | *How* are provider calls executed at runtime? |
|
|
17
|
+
| **SDK** (this) | *How* do callers reach the proxy over HTTP? |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Role in the system
|
|
22
|
+
|
|
23
|
+
The SDK implements **Path B** (runtime HTTP) from the [system overview](../README.md). Schema work (Path A) stays in core — the SDK does not expose deployment config or LLM call-schema endpoints.
|
|
24
|
+
|
|
25
|
+
```mermaid
|
|
26
|
+
flowchart LR
|
|
27
|
+
Caller["Caller service"]
|
|
28
|
+
SDK["unique_search_proxy_sdk"]
|
|
29
|
+
Core["unique_search_proxy_core"]
|
|
30
|
+
Proxy["unique_search_proxy_client"]
|
|
31
|
+
|
|
32
|
+
Caller --> SDK
|
|
33
|
+
SDK --> Core
|
|
34
|
+
SDK -->|"httpx"| Proxy
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Before a search call, the caller (or SDK) validates the payload against core request models. Non-2xx responses become the same `ProxyError` subclasses defined in core.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 3. Architecture
|
|
42
|
+
|
|
43
|
+
Two layers: a **hand-written facade** over **OpenAPI-generated code**.
|
|
44
|
+
|
|
45
|
+
```mermaid
|
|
46
|
+
flowchart TB
|
|
47
|
+
subgraph sdk_pkg["unique_search_proxy_sdk"]
|
|
48
|
+
Facade["UniqueSearchProxyClient"]
|
|
49
|
+
SC["SearchClient"]
|
|
50
|
+
ASC["AgentSearchClient"]
|
|
51
|
+
CC["CrawlClient"]
|
|
52
|
+
Transport["OpenapiTransport"]
|
|
53
|
+
Gen["_generated/"]
|
|
54
|
+
Conv["converters.py"]
|
|
55
|
+
Err["errors.py"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Core["unique_search_proxy_core"]
|
|
59
|
+
Proxy["Proxy pod"]
|
|
60
|
+
|
|
61
|
+
Facade --> SC & ASC & CC
|
|
62
|
+
SC & ASC & CC --> Transport
|
|
63
|
+
Transport --> Gen
|
|
64
|
+
SC --> Conv --> Core
|
|
65
|
+
Err --> Core
|
|
66
|
+
Gen -->|"httpx AsyncClient"| Proxy
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| Layer | Path | Responsibility |
|
|
70
|
+
|-------|------|----------------|
|
|
71
|
+
| **Facade** | `client.py` | `UniqueSearchProxyClient` — composes sub-clients, `health()`, `ready()` |
|
|
72
|
+
| **Sub-clients** | `search_client.py`, `agent_search_client.py`, `crawl_client.py` | Per-provider typed endpoints + compatibility dispatchers |
|
|
73
|
+
| **Endpoint factory** | `_endpoint.py` | `async_post_endpoint` / `async_sse_endpoint` (ParamSpec from core request models) |
|
|
74
|
+
| **Transport** | `_transport.py`, `_http.py` | Shared httpx lifecycle, base URL, timeout |
|
|
75
|
+
| **Generated** | `_generated/` | Route functions + attrs models from OpenAPI (**do not edit**) |
|
|
76
|
+
| **Converters** | `converters.py` | Core Pydantic models → generated SDK models |
|
|
77
|
+
| **Errors** | `errors.py` | Error envelope → core `ProxyError` subclasses |
|
|
78
|
+
|
|
79
|
+
The client package is the **source of truth** for the HTTP contract. When routes change, regenerate `_generated/` from `unique_search_proxy_client/openapi.json`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 4. Usage
|
|
84
|
+
|
|
85
|
+
Each provider exposes a typed endpoint whose kwargs mirror the core `*Request` Pydantic model (via `ParamSpec`). Snake_case and camelCase aliases both work (`fetch_size` / `fetchSize`).
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from unique_search_proxy_sdk import UniqueSearchProxyClient
|
|
89
|
+
|
|
90
|
+
async with UniqueSearchProxyClient("http://unique-search-proxy:2349") as client:
|
|
91
|
+
await client.health()
|
|
92
|
+
|
|
93
|
+
# Typed per-provider endpoints (preferred)
|
|
94
|
+
result = await client.search.google(query="unique ag", gl="ch", fetch_size=10)
|
|
95
|
+
brave = await client.search.brave(query="news", country="US", safesearch="strict")
|
|
96
|
+
agent = await client.agent_search.bing(query="EU AI Act timeline", fetch_size=5)
|
|
97
|
+
crawl = await client.crawl.basic(urls=["https://example.com"])
|
|
98
|
+
|
|
99
|
+
# Compatibility dispatchers (engine/crawler string)
|
|
100
|
+
result = await client.search.search("unique ag", engine="google", fetch_size=10)
|
|
101
|
+
agent = await client.agent_search.search("query", engine="bing")
|
|
102
|
+
crawl = await client.crawl.crawl(["https://example.com"], crawler="Basic")
|
|
103
|
+
|
|
104
|
+
# Agent SSE streaming
|
|
105
|
+
async for event in client.agent_search.bing_stream(query="query"):
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
# Low-level: one generated function per route
|
|
109
|
+
raw_client = client.openapi
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Search payloads are validated through core's `parse_search_request()` before serialization. Agent and crawl clients follow the same pattern.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 5. API mapping
|
|
117
|
+
|
|
118
|
+
| Facade method | HTTP |
|
|
119
|
+
|---------------|------|
|
|
120
|
+
| `health()` | `GET /health` |
|
|
121
|
+
| `ready()` | `GET /ready` |
|
|
122
|
+
| `search.google(...)` / `.brave(...)` / `.perplexity(...)` | `POST /v1/search` |
|
|
123
|
+
| `search.search(...)` | `POST /v1/search` (dispatcher) |
|
|
124
|
+
| `agent_search.bing(...)` / `.vertexai(...)` | `POST /v1/agent-search` |
|
|
125
|
+
| `agent_search.bing_stream(...)` / `.vertexai_stream(...)` | `POST /v1/agent-search/stream` (SSE) |
|
|
126
|
+
| `agent_search.search(...)` / `.stream(...)` | same routes (dispatchers) |
|
|
127
|
+
| `crawl.basic(...)` / `.tavily(...)` / `.jina(...)` / `.firecrawl(...)` | `POST /v1/crawl` |
|
|
128
|
+
| `crawl.crawl(...)` | `POST /v1/crawl` (dispatcher) |
|
|
129
|
+
| `openapi` | Low-level generated client (one function per route) |
|
|
130
|
+
|
|
131
|
+
Endpoint payloads and provider ids → [Client README](../unique_search_proxy_client/README.md).
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 6. Error handling
|
|
136
|
+
|
|
137
|
+
Non-success responses with a proxy error envelope raise typed exceptions from core:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from unique_search_proxy_core import EngineNotConfiguredError, UpstreamError
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`ENGINE_NOT_CONFIGURED` (503) includes `missing_env_vars` parsed from the error `details` list — useful for operators and LLM tool consumers.
|
|
144
|
+
|
|
145
|
+
Transport failures (non-JSON body, connection errors) raise `UniqueSearchProxyClientError`.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 7. Codegen workflow
|
|
150
|
+
|
|
151
|
+
Generated via [openapi-python-client](https://github.com/openapi-generators/openapi-python-client) from the server's OpenAPI spec.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cd ../unique_search_proxy_client
|
|
155
|
+
uv sync
|
|
156
|
+
uv run python scripts/generate_sdk.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
| Path | Role |
|
|
160
|
+
|------|------|
|
|
161
|
+
| `unique_search_proxy_sdk/_generated/` | Regenerated httpx client + attrs models |
|
|
162
|
+
| `unique_search_proxy_client/openapi.json` | Exported spec (codegen input) |
|
|
163
|
+
| `unique_search_proxy_sdk/client.py` | Hand-written facade (not regenerated) |
|
|
164
|
+
|
|
165
|
+
Other codegen tools considered: OpenAPI Generator, datamodel-code-generator, Kiota — see git history in the client README if needed.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 8. Testing
|
|
170
|
+
|
|
171
|
+
Pass an injected `httpx.AsyncClient` with `ASGITransport` for in-process tests against `create_app()`:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
import httpx
|
|
175
|
+
from httpx import ASGITransport
|
|
176
|
+
from unique_search_proxy_client.web.app import create_app
|
|
177
|
+
from unique_search_proxy_sdk import UniqueSearchProxyClient
|
|
178
|
+
|
|
179
|
+
app = create_app()
|
|
180
|
+
async with httpx.AsyncClient(
|
|
181
|
+
transport=ASGITransport(app=app),
|
|
182
|
+
base_url="http://test",
|
|
183
|
+
) as http:
|
|
184
|
+
async with UniqueSearchProxyClient("http://test", http_client=http) as client:
|
|
185
|
+
...
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Run the app lifespan so the in-process `HttpClientPool` is initialised.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 9. Installation & development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
cd unique_search_proxy_sdk
|
|
196
|
+
uv sync
|
|
197
|
+
uv run pytest
|
|
198
|
+
uv run ruff check .
|
|
199
|
+
uv run basedpyright
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Depends on `unique-search-proxy-core`. Does not depend on `unique-search-proxy` at runtime (only as a dev dependency for integration tests).
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
Proprietary — Unique AG
|
{unique_search_proxy_sdk-2026.26.0.dev4 → unique_search_proxy_sdk-2026.26.0.dev5}/pyproject.toml
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique-search-proxy-sdk"
|
|
3
|
-
version = "2026.26.0.
|
|
3
|
+
version = "2026.26.0.dev5"
|
|
4
4
|
description = "Async HTTP client for the Unique Search Proxy API"
|
|
5
5
|
authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -9,7 +9,7 @@ dependencies = [
|
|
|
9
9
|
"attrs>=24.3.0,<26",
|
|
10
10
|
"httpx>=0.28.0,<0.29.0",
|
|
11
11
|
"pydantic>=2.12.5,<3.0.0",
|
|
12
|
-
"unique-search-proxy-core>=2026.26.0.
|
|
12
|
+
"unique-search-proxy-core>=2026.26.0.dev5,<2026.26.0rc0",
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
[dependency-groups]
|
|
@@ -71,3 +71,9 @@ markers = [
|
|
|
71
71
|
"asyncio: asyncio tests",
|
|
72
72
|
"integration: requires unique-search-proxy (FastAPI app) in the environment",
|
|
73
73
|
]
|
|
74
|
+
|
|
75
|
+
[tool.coverage.run]
|
|
76
|
+
omit = [
|
|
77
|
+
"unique_search_proxy_sdk/_generated/*",
|
|
78
|
+
"unique_search_proxy_sdk/typing_checks.py",
|
|
79
|
+
]
|
|
@@ -13,6 +13,7 @@ Regenerate after API changes::
|
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from unique_search_proxy_sdk._generated.client import Client as OpenAPIClient
|
|
16
|
+
from unique_search_proxy_sdk.agent_search_client import AgentSearchClient
|
|
16
17
|
from unique_search_proxy_sdk.client import UniqueSearchProxyClient
|
|
17
18
|
from unique_search_proxy_sdk.crawl_client import CrawlClient
|
|
18
19
|
from unique_search_proxy_sdk.errors import UniqueSearchProxyClientError
|
|
@@ -20,6 +21,7 @@ from unique_search_proxy_sdk.search_client import SearchClient
|
|
|
20
21
|
|
|
21
22
|
__all__ = [
|
|
22
23
|
"CrawlClient",
|
|
24
|
+
"AgentSearchClient",
|
|
23
25
|
"OpenAPIClient",
|
|
24
26
|
"SearchClient",
|
|
25
27
|
"UniqueSearchProxyClient",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Async endpoint factories with ParamSpec signatures from core request models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
7
|
+
from typing import Any, ParamSpec, Protocol, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from unique_search_proxy_sdk._http import unwrap_response
|
|
13
|
+
from unique_search_proxy_sdk._transport import OpenapiTransport
|
|
14
|
+
from unique_search_proxy_sdk.errors import raise_for_proxy_response
|
|
15
|
+
|
|
16
|
+
ResponseType = TypeVar("ResponseType")
|
|
17
|
+
RequestConstructorSpec = ParamSpec("RequestConstructorSpec")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _kwargs_without_none(**kwargs: Any) -> dict[str, Any]:
|
|
21
|
+
return {key: value for key, value in kwargs.items() if value is not None}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SdkBody(Protocol):
|
|
25
|
+
def to_dict(self) -> dict[str, Any]: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def async_post_endpoint(
|
|
29
|
+
transport: OpenapiTransport,
|
|
30
|
+
request_model: Callable[RequestConstructorSpec, BaseModel],
|
|
31
|
+
*,
|
|
32
|
+
parse: Callable[[object], BaseModel],
|
|
33
|
+
to_sdk: Callable[[BaseModel], SdkBody],
|
|
34
|
+
post: Callable[..., Awaitable[Any]],
|
|
35
|
+
response_type: type[ResponseType],
|
|
36
|
+
) -> Callable[RequestConstructorSpec, Awaitable[ResponseType]]:
|
|
37
|
+
"""Build an async POST caller whose kwargs match ``request_model``."""
|
|
38
|
+
_ = response_type
|
|
39
|
+
|
|
40
|
+
async def call(
|
|
41
|
+
*args: RequestConstructorSpec.args,
|
|
42
|
+
**kwargs: RequestConstructorSpec.kwargs,
|
|
43
|
+
) -> ResponseType:
|
|
44
|
+
constructed = request_model(*args, **_kwargs_without_none(**kwargs))
|
|
45
|
+
validated = parse(
|
|
46
|
+
constructed.model_dump(mode="json", by_alias=True, exclude_none=True),
|
|
47
|
+
)
|
|
48
|
+
sdk_body = to_sdk(validated)
|
|
49
|
+
response = await post(client=transport.openapi, body=sdk_body)
|
|
50
|
+
return cast(ResponseType, unwrap_response(response))
|
|
51
|
+
|
|
52
|
+
return call
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def async_sse_endpoint(
|
|
56
|
+
transport: OpenapiTransport,
|
|
57
|
+
path: str,
|
|
58
|
+
request_model: Callable[RequestConstructorSpec, BaseModel],
|
|
59
|
+
*,
|
|
60
|
+
parse: Callable[[object], BaseModel],
|
|
61
|
+
to_sdk: Callable[[BaseModel], SdkBody],
|
|
62
|
+
) -> Callable[RequestConstructorSpec, AsyncIterator[dict[str, Any]]]:
|
|
63
|
+
"""Build an async SSE stream caller whose kwargs match ``request_model``."""
|
|
64
|
+
|
|
65
|
+
async def stream(
|
|
66
|
+
*args: RequestConstructorSpec.args,
|
|
67
|
+
**kwargs: RequestConstructorSpec.kwargs,
|
|
68
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
69
|
+
constructed = request_model(*args, **_kwargs_without_none(**kwargs))
|
|
70
|
+
validated = parse(
|
|
71
|
+
constructed.model_dump(mode="json", by_alias=True, exclude_none=True),
|
|
72
|
+
)
|
|
73
|
+
sdk_body = to_sdk(validated)
|
|
74
|
+
http_client = transport.openapi.get_async_httpx_client()
|
|
75
|
+
base_url = transport.base_url
|
|
76
|
+
async with http_client.stream(
|
|
77
|
+
"POST",
|
|
78
|
+
f"{base_url}{path}",
|
|
79
|
+
json=sdk_body.to_dict(),
|
|
80
|
+
headers={"Content-Type": "application/json"},
|
|
81
|
+
) as response:
|
|
82
|
+
if response.status_code >= 400:
|
|
83
|
+
raw = await response.aread()
|
|
84
|
+
raise_for_proxy_response(
|
|
85
|
+
httpx.Response(
|
|
86
|
+
status_code=response.status_code,
|
|
87
|
+
content=raw,
|
|
88
|
+
headers=dict(response.headers),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
async for line in response.aiter_lines():
|
|
92
|
+
if not line.startswith("data: "):
|
|
93
|
+
continue
|
|
94
|
+
yield json.loads(line.removeprefix("data: "))
|
|
95
|
+
|
|
96
|
+
return stream
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
__all__ = ["async_post_endpoint", "async_sse_endpoint"]
|