Skip to content

Commit 00b17c1

Browse files
esteiningerclaude
andcommitted
feat: add high-level Mixpeek client wrapper (v0.82.0)
Adds mixpeek/_client/ with ergonomic one-liner DX: - Mixpeek("sk_xxx") client with .search(), .index() convenience methods - Resource managers: .namespaces, .buckets, .collections, .retrievers, .documents - Uses urllib3 directly (no new deps), reads env vars for defaults - Also fixes null -> None in 5 generated model files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e0d0bc3 commit 00b17c1

12 files changed

Lines changed: 437 additions & 11 deletions

mixpeek/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
""" # noqa: E501
1616

1717

18-
__version__ = "0.81.0"
18+
__version__ = "0.82.0"
19+
20+
# High-level client
21+
from mixpeek._client import Mixpeek as Mixpeek # noqa: E402
1922

2023
# Define package exports
2124
__all__ = [

mixpeek/_client/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from mixpeek._client.client import Mixpeek
2+
3+
__all__ = ["Mixpeek"]

mixpeek/_client/client.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"""High-level Mixpeek client — ergonomic wrapper around the generated SDK."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import os
7+
from typing import Any, Dict, List, Optional
8+
9+
import urllib3
10+
11+
12+
class Mixpeek:
13+
"""One-liner client for the Mixpeek API.
14+
15+
Usage::
16+
17+
from mixpeek import Mixpeek
18+
19+
mp = Mixpeek("sk_xxx", namespace="ns_xxx")
20+
results = mp.search("red car", collection="products")
21+
"""
22+
23+
DEFAULT_BASE_URL = "https://api.mixpeek.com/v1"
24+
25+
def __init__(
26+
self,
27+
api_key: Optional[str] = None,
28+
*,
29+
namespace: Optional[str] = None,
30+
base_url: Optional[str] = None,
31+
timeout: float = 30.0,
32+
) -> None:
33+
self.api_key = api_key or os.environ.get("MIXPEEK_API_KEY", "")
34+
if not self.api_key:
35+
raise ValueError(
36+
"An API key is required. Pass api_key= or set MIXPEEK_API_KEY."
37+
)
38+
self.namespace = namespace or os.environ.get("MIXPEEK_NAMESPACE")
39+
self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
40+
self.timeout = timeout
41+
self._http = urllib3.PoolManager(
42+
timeout=urllib3.Timeout(connect=5.0, read=timeout),
43+
retries=urllib3.Retry(total=2, backoff_factor=0.3),
44+
)
45+
46+
# Resource managers (lazy-style but immediate for discoverability)
47+
from mixpeek._client.resources import (
48+
Buckets,
49+
Collections,
50+
Documents,
51+
Namespaces,
52+
Retrievers,
53+
)
54+
55+
self.namespaces = Namespaces(self)
56+
self.buckets = Buckets(self)
57+
self.collections = Collections(self)
58+
self.retrievers = Retrievers(self)
59+
self.documents = Documents(self)
60+
61+
# ---- Convenience shortcuts ------------------------------------------------
62+
63+
def search(
64+
self,
65+
query: str,
66+
*,
67+
collection: Optional[str] = None,
68+
filters: Optional[Dict[str, Any]] = None,
69+
limit: int = 10,
70+
namespace: Optional[str] = None,
71+
) -> Dict[str, Any]:
72+
"""Zero-setup semantic search using an adhoc retriever.
73+
74+
Constructs a ``feature_search`` stage on the fly and executes it
75+
via ``POST /v1/retrievers/execute`` (adhoc mode).
76+
"""
77+
ns = namespace or self.namespace
78+
stages: List[Dict[str, Any]] = [
79+
{
80+
"type": "feature_search",
81+
"feature_extractor": {"type": "text"},
82+
"query": query,
83+
"collection_ids": [collection] if collection else [],
84+
"limit": limit,
85+
}
86+
]
87+
if filters:
88+
stages.insert(
89+
0,
90+
{
91+
"type": "attribute_filter",
92+
"conditions": filters,
93+
},
94+
)
95+
body: Dict[str, Any] = {
96+
"stages": stages,
97+
"inputs": {"query": query},
98+
}
99+
return self._request(
100+
"POST", "/retrievers/execute", body=body, namespace=ns
101+
)
102+
103+
def index(
104+
self,
105+
source: str,
106+
*,
107+
collection: Optional[str] = None,
108+
metadata: Optional[Dict[str, Any]] = None,
109+
namespace: Optional[str] = None,
110+
) -> Dict[str, Any]:
111+
"""Upload a file URL to a bucket and optionally trigger a collection.
112+
113+
``source`` can be an S3/GCS URI or any public URL.
114+
If ``collection`` is provided the collection is triggered after upload.
115+
"""
116+
ns = namespace or self.namespace
117+
118+
# Find or pick the first bucket in the namespace
119+
buckets = self._request("POST", "/buckets/list", namespace=ns)
120+
if not buckets:
121+
raise ValueError("No buckets found in the target namespace.")
122+
bucket_id = buckets[0]["bucket_id"] if isinstance(buckets, list) else buckets["results"][0]["bucket_id"]
123+
124+
upload_body: Dict[str, Any] = {"blob": {"url": source}}
125+
if metadata:
126+
upload_body["metadata"] = metadata
127+
128+
result = self._request(
129+
"POST", f"/buckets/{bucket_id}/objects", body=upload_body, namespace=ns
130+
)
131+
132+
if collection:
133+
self._request(
134+
"POST", f"/collections/{collection}/trigger", namespace=ns
135+
)
136+
137+
return result
138+
139+
# ---- HTTP helper ----------------------------------------------------------
140+
141+
def _request(
142+
self,
143+
method: str,
144+
path: str,
145+
*,
146+
body: Optional[Any] = None,
147+
namespace: Optional[str] = None,
148+
) -> Any:
149+
url = f"{self.base_url}{path}"
150+
headers = {
151+
"Authorization": f"Bearer {self.api_key}",
152+
"Content-Type": "application/json",
153+
"Accept": "application/json",
154+
}
155+
ns = namespace or self.namespace
156+
if ns:
157+
headers["X-Namespace"] = ns
158+
159+
encoded_body = json.dumps(body).encode("utf-8") if body is not None else None
160+
161+
resp = self._http.request(
162+
method,
163+
url,
164+
body=encoded_body,
165+
headers=headers,
166+
)
167+
168+
if resp.status >= 400:
169+
try:
170+
detail = json.loads(resp.data.decode("utf-8"))
171+
except Exception:
172+
detail = resp.data.decode("utf-8", errors="replace")
173+
raise MixpeekAPIError(resp.status, detail)
174+
175+
if not resp.data:
176+
return None
177+
178+
return json.loads(resp.data.decode("utf-8"))
179+
180+
181+
class MixpeekAPIError(Exception):
182+
"""Raised when the Mixpeek API returns an error response."""
183+
184+
def __init__(self, status: int, detail: Any) -> None:
185+
self.status = status
186+
self.detail = detail
187+
super().__init__(f"Mixpeek API error {status}: {detail}")

0 commit comments

Comments
 (0)