fluxvector 0.1.0__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.
- fluxvector-0.1.0/PKG-INFO +250 -0
- fluxvector-0.1.0/README.md +218 -0
- fluxvector-0.1.0/fluxvector/__init__.py +49 -0
- fluxvector-0.1.0/fluxvector/_version.py +1 -0
- fluxvector-0.1.0/fluxvector/async_client.py +139 -0
- fluxvector-0.1.0/fluxvector/client.py +139 -0
- fluxvector-0.1.0/fluxvector/errors.py +89 -0
- fluxvector-0.1.0/fluxvector/resources/__init__.py +26 -0
- fluxvector-0.1.0/fluxvector/resources/api_keys.py +71 -0
- fluxvector-0.1.0/fluxvector/resources/collections.py +113 -0
- fluxvector-0.1.0/fluxvector/resources/embeddings.py +45 -0
- fluxvector-0.1.0/fluxvector/resources/search.py +69 -0
- fluxvector-0.1.0/fluxvector/resources/usage.py +45 -0
- fluxvector-0.1.0/fluxvector/resources/vectors.py +181 -0
- fluxvector-0.1.0/fluxvector/types.py +251 -0
- fluxvector-0.1.0/pyproject.toml +60 -0
- fluxvector-0.1.0/tests/__init__.py +0 -0
- fluxvector-0.1.0/tests/test_client.py +447 -0
- fluxvector-0.1.0/tests/test_types.py +204 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluxvector
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for FluxVector — semantic search API by FluxSoft Labs
|
|
5
|
+
Project-URL: Homepage, https://fluxsoftlabs.com/fluxvector
|
|
6
|
+
Project-URL: Documentation, https://docs.fluxsoftlabs.com/fluxvector
|
|
7
|
+
Project-URL: Repository, https://github.com/fluxsoftlabs/fluxvector-python
|
|
8
|
+
Project-URL: Issues, https://github.com/fluxsoftlabs/fluxvector-python/issues
|
|
9
|
+
Author-email: FluxSoft Labs <hello@fluxsoftlabs.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: ai,embeddings,fluxvector,semantic-search,vector
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: httpx>=0.25.0
|
|
24
|
+
Requires-Dist: pydantic>=2.0.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# FluxVector Python SDK
|
|
34
|
+
|
|
35
|
+
Official Python SDK for [FluxVector](https://fluxsoftlabs.com/fluxvector) — semantic search API by FluxSoft Labs.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install fluxvector
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from fluxvector import FluxVector
|
|
47
|
+
|
|
48
|
+
fv = FluxVector(api_key="fv_live_abc123")
|
|
49
|
+
|
|
50
|
+
# Create a collection
|
|
51
|
+
col = fv.collections.create("products", dimension=1536, metric="cosine")
|
|
52
|
+
|
|
53
|
+
# Upsert vectors (auto-chunks at 1000)
|
|
54
|
+
fv.vectors.upsert("products", [
|
|
55
|
+
{"id": "p1", "text": "Red running shoes", "metadata": {"price": 89, "brand": "Nike"}},
|
|
56
|
+
{"id": "p2", "text": "Blue hiking boots", "metadata": {"price": 149, "brand": "Merrell"}},
|
|
57
|
+
{"id": "p3", "text": "White tennis sneakers", "metadata": {"price": 65, "brand": "Adidas"}},
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
# Semantic search
|
|
61
|
+
results = fv.search("products", "comfortable shoes for running", top_k=5)
|
|
62
|
+
for r in results:
|
|
63
|
+
print(f"{r.id}: {r.score:.2f} — {r.text}")
|
|
64
|
+
|
|
65
|
+
# Search with metadata filters
|
|
66
|
+
results = fv.search("products", "shoes", filter={"price": {"$lt": 100}})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Async Support
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from fluxvector import AsyncFluxVector
|
|
73
|
+
|
|
74
|
+
async with AsyncFluxVector(api_key="fv_live_abc123") as fv:
|
|
75
|
+
results = await fv.search("products", "comfortable shoes")
|
|
76
|
+
for r in results:
|
|
77
|
+
print(f"{r.id}: {r.score:.2f}")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
fv = FluxVector(
|
|
84
|
+
api_key="fv_live_abc123", # or set FLUXVECTOR_API_KEY env var
|
|
85
|
+
base_url="https://custom.host", # default: https://fluxvector.dev
|
|
86
|
+
timeout=30.0, # request timeout in seconds
|
|
87
|
+
max_retries=3, # retries on 429 / 5xx with exponential backoff
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
### Collections
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# Create
|
|
97
|
+
col = fv.collections.create("products", dimension=1536, metric="cosine", description="Product catalog")
|
|
98
|
+
|
|
99
|
+
# List (cursor pagination)
|
|
100
|
+
page = fv.collections.list(limit=10)
|
|
101
|
+
for col in page:
|
|
102
|
+
print(col.name)
|
|
103
|
+
# Next page
|
|
104
|
+
if page.has_more:
|
|
105
|
+
next_page = fv.collections.list(cursor=page.next_cursor)
|
|
106
|
+
|
|
107
|
+
# Get
|
|
108
|
+
col = fv.collections.get("products")
|
|
109
|
+
|
|
110
|
+
# Delete
|
|
111
|
+
fv.collections.delete("products")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Vectors
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
# Upsert (auto-chunks at 1000 vectors per request)
|
|
118
|
+
fv.vectors.upsert("products", [
|
|
119
|
+
{"id": "p1", "text": "Red shoes", "metadata": {"price": 89}},
|
|
120
|
+
{"id": "p2", "text": "Blue hat", "values": [0.1, 0.2, ...]}, # raw vector
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
# Query by text
|
|
124
|
+
results = fv.vectors.query("products", text="shoes", top_k=10)
|
|
125
|
+
|
|
126
|
+
# Query by raw vector
|
|
127
|
+
results = fv.vectors.query("products", vector=[0.1, 0.2, ...], top_k=5)
|
|
128
|
+
|
|
129
|
+
# Query with filter
|
|
130
|
+
results = fv.vectors.query(
|
|
131
|
+
"products",
|
|
132
|
+
text="shoes",
|
|
133
|
+
filter={"brand": {"$in": ["Nike", "Adidas"]}},
|
|
134
|
+
include_metadata=True,
|
|
135
|
+
include_text=True,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Fetch by IDs
|
|
139
|
+
vectors = fv.vectors.fetch("products", ["p1", "p2"])
|
|
140
|
+
|
|
141
|
+
# Delete by IDs
|
|
142
|
+
fv.vectors.delete("products", ids=["p1", "p2"])
|
|
143
|
+
|
|
144
|
+
# Delete by filter
|
|
145
|
+
fv.vectors.delete("products", filter={"brand": {"$eq": "discontinued"}})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Search
|
|
149
|
+
|
|
150
|
+
The signature method — one-line semantic search:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
results = fv.search("products", "comfortable running shoes", top_k=10)
|
|
154
|
+
for r in results:
|
|
155
|
+
print(f"{r.id}: {r.score:.4f} — {r.text}")
|
|
156
|
+
print(f" metadata: {r.metadata}")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Embeddings
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
# Single text
|
|
163
|
+
resp = fv.embeddings.create("Hello world")
|
|
164
|
+
print(resp.embedding) # [0.012, -0.034, ...]
|
|
165
|
+
print(resp.dimension) # 1536
|
|
166
|
+
|
|
167
|
+
# Batch
|
|
168
|
+
resp = fv.embeddings.batch(["Hello", "World", "Foo"])
|
|
169
|
+
for emb in resp.embeddings:
|
|
170
|
+
print(len(emb)) # 1536
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### API Keys
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
# Create
|
|
177
|
+
key = fv.api_keys.create("Production Key", env="live")
|
|
178
|
+
print(key.key) # fv_live_... (only shown once)
|
|
179
|
+
|
|
180
|
+
# List
|
|
181
|
+
keys = fv.api_keys.list()
|
|
182
|
+
for k in keys:
|
|
183
|
+
print(f"{k.name}: {k.prefix}...")
|
|
184
|
+
|
|
185
|
+
# Rename
|
|
186
|
+
fv.api_keys.update("key_id", name="New Name")
|
|
187
|
+
|
|
188
|
+
# Delete
|
|
189
|
+
fv.api_keys.delete("key_id")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Usage
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
# Current usage
|
|
196
|
+
usage = fv.usage.get()
|
|
197
|
+
print(f"Plan: {usage.plan}")
|
|
198
|
+
print(f"Requests: {usage.requests}")
|
|
199
|
+
print(f"Vectors stored: {usage.vectors_stored}")
|
|
200
|
+
|
|
201
|
+
# Historical usage
|
|
202
|
+
history = fv.usage.history(days=30)
|
|
203
|
+
for day in history:
|
|
204
|
+
print(f"{day.date}: {day.requests} requests")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Filter Operators
|
|
208
|
+
|
|
209
|
+
Use metadata filters with any search or query method:
|
|
210
|
+
|
|
211
|
+
| Operator | Description | Example |
|
|
212
|
+
|----------|-------------|---------|
|
|
213
|
+
| `$eq` | Equal | `{"status": {"$eq": "active"}}` |
|
|
214
|
+
| `$ne` | Not equal | `{"status": {"$ne": "deleted"}}` |
|
|
215
|
+
| `$gt` | Greater than | `{"price": {"$gt": 50}}` |
|
|
216
|
+
| `$gte` | Greater than or equal | `{"price": {"$gte": 50}}` |
|
|
217
|
+
| `$lt` | Less than | `{"price": {"$lt": 100}}` |
|
|
218
|
+
| `$lte` | Less than or equal | `{"price": {"$lte": 100}}` |
|
|
219
|
+
| `$in` | In array | `{"brand": {"$in": ["Nike", "Adidas"]}}` |
|
|
220
|
+
| `$nin` | Not in array | `{"brand": {"$nin": ["Generic"]}}` |
|
|
221
|
+
|
|
222
|
+
## Error Handling
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from fluxvector import FluxVector, FluxVectorError, AuthenticationError, RateLimitError, NotFoundError
|
|
226
|
+
|
|
227
|
+
fv = FluxVector(api_key="fv_live_abc123")
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
results = fv.search("products", "shoes")
|
|
231
|
+
except AuthenticationError:
|
|
232
|
+
print("Invalid API key")
|
|
233
|
+
except RateLimitError as e:
|
|
234
|
+
print(f"Rate limited: {e.message}")
|
|
235
|
+
except NotFoundError:
|
|
236
|
+
print("Collection not found")
|
|
237
|
+
except FluxVectorError as e:
|
|
238
|
+
print(f"API error {e.status_code}: {e.message}")
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Environment Variables
|
|
242
|
+
|
|
243
|
+
| Variable | Description |
|
|
244
|
+
|----------|-------------|
|
|
245
|
+
| `FLUXVECTOR_API_KEY` | Default API key (if not passed to constructor) |
|
|
246
|
+
| `FLUXVECTOR_BASE_URL` | Override base URL for self-hosted instances |
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# FluxVector Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [FluxVector](https://fluxsoftlabs.com/fluxvector) — semantic search API by FluxSoft Labs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install fluxvector
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from fluxvector import FluxVector
|
|
15
|
+
|
|
16
|
+
fv = FluxVector(api_key="fv_live_abc123")
|
|
17
|
+
|
|
18
|
+
# Create a collection
|
|
19
|
+
col = fv.collections.create("products", dimension=1536, metric="cosine")
|
|
20
|
+
|
|
21
|
+
# Upsert vectors (auto-chunks at 1000)
|
|
22
|
+
fv.vectors.upsert("products", [
|
|
23
|
+
{"id": "p1", "text": "Red running shoes", "metadata": {"price": 89, "brand": "Nike"}},
|
|
24
|
+
{"id": "p2", "text": "Blue hiking boots", "metadata": {"price": 149, "brand": "Merrell"}},
|
|
25
|
+
{"id": "p3", "text": "White tennis sneakers", "metadata": {"price": 65, "brand": "Adidas"}},
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
# Semantic search
|
|
29
|
+
results = fv.search("products", "comfortable shoes for running", top_k=5)
|
|
30
|
+
for r in results:
|
|
31
|
+
print(f"{r.id}: {r.score:.2f} — {r.text}")
|
|
32
|
+
|
|
33
|
+
# Search with metadata filters
|
|
34
|
+
results = fv.search("products", "shoes", filter={"price": {"$lt": 100}})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Async Support
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from fluxvector import AsyncFluxVector
|
|
41
|
+
|
|
42
|
+
async with AsyncFluxVector(api_key="fv_live_abc123") as fv:
|
|
43
|
+
results = await fv.search("products", "comfortable shoes")
|
|
44
|
+
for r in results:
|
|
45
|
+
print(f"{r.id}: {r.score:.2f}")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
fv = FluxVector(
|
|
52
|
+
api_key="fv_live_abc123", # or set FLUXVECTOR_API_KEY env var
|
|
53
|
+
base_url="https://custom.host", # default: https://fluxvector.dev
|
|
54
|
+
timeout=30.0, # request timeout in seconds
|
|
55
|
+
max_retries=3, # retries on 429 / 5xx with exponential backoff
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API Reference
|
|
60
|
+
|
|
61
|
+
### Collections
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# Create
|
|
65
|
+
col = fv.collections.create("products", dimension=1536, metric="cosine", description="Product catalog")
|
|
66
|
+
|
|
67
|
+
# List (cursor pagination)
|
|
68
|
+
page = fv.collections.list(limit=10)
|
|
69
|
+
for col in page:
|
|
70
|
+
print(col.name)
|
|
71
|
+
# Next page
|
|
72
|
+
if page.has_more:
|
|
73
|
+
next_page = fv.collections.list(cursor=page.next_cursor)
|
|
74
|
+
|
|
75
|
+
# Get
|
|
76
|
+
col = fv.collections.get("products")
|
|
77
|
+
|
|
78
|
+
# Delete
|
|
79
|
+
fv.collections.delete("products")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Vectors
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
# Upsert (auto-chunks at 1000 vectors per request)
|
|
86
|
+
fv.vectors.upsert("products", [
|
|
87
|
+
{"id": "p1", "text": "Red shoes", "metadata": {"price": 89}},
|
|
88
|
+
{"id": "p2", "text": "Blue hat", "values": [0.1, 0.2, ...]}, # raw vector
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
# Query by text
|
|
92
|
+
results = fv.vectors.query("products", text="shoes", top_k=10)
|
|
93
|
+
|
|
94
|
+
# Query by raw vector
|
|
95
|
+
results = fv.vectors.query("products", vector=[0.1, 0.2, ...], top_k=5)
|
|
96
|
+
|
|
97
|
+
# Query with filter
|
|
98
|
+
results = fv.vectors.query(
|
|
99
|
+
"products",
|
|
100
|
+
text="shoes",
|
|
101
|
+
filter={"brand": {"$in": ["Nike", "Adidas"]}},
|
|
102
|
+
include_metadata=True,
|
|
103
|
+
include_text=True,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Fetch by IDs
|
|
107
|
+
vectors = fv.vectors.fetch("products", ["p1", "p2"])
|
|
108
|
+
|
|
109
|
+
# Delete by IDs
|
|
110
|
+
fv.vectors.delete("products", ids=["p1", "p2"])
|
|
111
|
+
|
|
112
|
+
# Delete by filter
|
|
113
|
+
fv.vectors.delete("products", filter={"brand": {"$eq": "discontinued"}})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Search
|
|
117
|
+
|
|
118
|
+
The signature method — one-line semantic search:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
results = fv.search("products", "comfortable running shoes", top_k=10)
|
|
122
|
+
for r in results:
|
|
123
|
+
print(f"{r.id}: {r.score:.4f} — {r.text}")
|
|
124
|
+
print(f" metadata: {r.metadata}")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Embeddings
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
# Single text
|
|
131
|
+
resp = fv.embeddings.create("Hello world")
|
|
132
|
+
print(resp.embedding) # [0.012, -0.034, ...]
|
|
133
|
+
print(resp.dimension) # 1536
|
|
134
|
+
|
|
135
|
+
# Batch
|
|
136
|
+
resp = fv.embeddings.batch(["Hello", "World", "Foo"])
|
|
137
|
+
for emb in resp.embeddings:
|
|
138
|
+
print(len(emb)) # 1536
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### API Keys
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
# Create
|
|
145
|
+
key = fv.api_keys.create("Production Key", env="live")
|
|
146
|
+
print(key.key) # fv_live_... (only shown once)
|
|
147
|
+
|
|
148
|
+
# List
|
|
149
|
+
keys = fv.api_keys.list()
|
|
150
|
+
for k in keys:
|
|
151
|
+
print(f"{k.name}: {k.prefix}...")
|
|
152
|
+
|
|
153
|
+
# Rename
|
|
154
|
+
fv.api_keys.update("key_id", name="New Name")
|
|
155
|
+
|
|
156
|
+
# Delete
|
|
157
|
+
fv.api_keys.delete("key_id")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Usage
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# Current usage
|
|
164
|
+
usage = fv.usage.get()
|
|
165
|
+
print(f"Plan: {usage.plan}")
|
|
166
|
+
print(f"Requests: {usage.requests}")
|
|
167
|
+
print(f"Vectors stored: {usage.vectors_stored}")
|
|
168
|
+
|
|
169
|
+
# Historical usage
|
|
170
|
+
history = fv.usage.history(days=30)
|
|
171
|
+
for day in history:
|
|
172
|
+
print(f"{day.date}: {day.requests} requests")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Filter Operators
|
|
176
|
+
|
|
177
|
+
Use metadata filters with any search or query method:
|
|
178
|
+
|
|
179
|
+
| Operator | Description | Example |
|
|
180
|
+
|----------|-------------|---------|
|
|
181
|
+
| `$eq` | Equal | `{"status": {"$eq": "active"}}` |
|
|
182
|
+
| `$ne` | Not equal | `{"status": {"$ne": "deleted"}}` |
|
|
183
|
+
| `$gt` | Greater than | `{"price": {"$gt": 50}}` |
|
|
184
|
+
| `$gte` | Greater than or equal | `{"price": {"$gte": 50}}` |
|
|
185
|
+
| `$lt` | Less than | `{"price": {"$lt": 100}}` |
|
|
186
|
+
| `$lte` | Less than or equal | `{"price": {"$lte": 100}}` |
|
|
187
|
+
| `$in` | In array | `{"brand": {"$in": ["Nike", "Adidas"]}}` |
|
|
188
|
+
| `$nin` | Not in array | `{"brand": {"$nin": ["Generic"]}}` |
|
|
189
|
+
|
|
190
|
+
## Error Handling
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from fluxvector import FluxVector, FluxVectorError, AuthenticationError, RateLimitError, NotFoundError
|
|
194
|
+
|
|
195
|
+
fv = FluxVector(api_key="fv_live_abc123")
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
results = fv.search("products", "shoes")
|
|
199
|
+
except AuthenticationError:
|
|
200
|
+
print("Invalid API key")
|
|
201
|
+
except RateLimitError as e:
|
|
202
|
+
print(f"Rate limited: {e.message}")
|
|
203
|
+
except NotFoundError:
|
|
204
|
+
print("Collection not found")
|
|
205
|
+
except FluxVectorError as e:
|
|
206
|
+
print(f"API error {e.status_code}: {e.message}")
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Environment Variables
|
|
210
|
+
|
|
211
|
+
| Variable | Description |
|
|
212
|
+
|----------|-------------|
|
|
213
|
+
| `FLUXVECTOR_API_KEY` | Default API key (if not passed to constructor) |
|
|
214
|
+
| `FLUXVECTOR_BASE_URL` | Override base URL for self-hosted instances |
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""FluxVector Python SDK — Semantic search by FluxSoft Labs."""
|
|
2
|
+
|
|
3
|
+
from fluxvector._version import __version__
|
|
4
|
+
from fluxvector.async_client import AsyncFluxVector
|
|
5
|
+
from fluxvector.client import FluxVector
|
|
6
|
+
from fluxvector.errors import (
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
FluxVectorError,
|
|
9
|
+
InternalServerError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
PermissionDeniedError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
from fluxvector.types import (
|
|
16
|
+
ApiKey,
|
|
17
|
+
Collection,
|
|
18
|
+
EmbedBatchResponse,
|
|
19
|
+
EmbedResponse,
|
|
20
|
+
SearchResponse,
|
|
21
|
+
SearchResult,
|
|
22
|
+
UpsertResponse,
|
|
23
|
+
UsageSummary,
|
|
24
|
+
Vector,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"__version__",
|
|
29
|
+
"FluxVector",
|
|
30
|
+
"AsyncFluxVector",
|
|
31
|
+
# Errors
|
|
32
|
+
"FluxVectorError",
|
|
33
|
+
"AuthenticationError",
|
|
34
|
+
"NotFoundError",
|
|
35
|
+
"PermissionDeniedError",
|
|
36
|
+
"RateLimitError",
|
|
37
|
+
"ValidationError",
|
|
38
|
+
"InternalServerError",
|
|
39
|
+
# Types
|
|
40
|
+
"ApiKey",
|
|
41
|
+
"Collection",
|
|
42
|
+
"EmbedBatchResponse",
|
|
43
|
+
"EmbedResponse",
|
|
44
|
+
"SearchResponse",
|
|
45
|
+
"SearchResult",
|
|
46
|
+
"UpsertResponse",
|
|
47
|
+
"UsageSummary",
|
|
48
|
+
"Vector",
|
|
49
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Asynchronous FluxVector client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from fluxvector._version import __version__
|
|
12
|
+
from fluxvector.errors import ConnectionError as FVConnectionError
|
|
13
|
+
from fluxvector.errors import raise_for_status
|
|
14
|
+
from fluxvector.resources.api_keys import AsyncApiKeysResource
|
|
15
|
+
from fluxvector.resources.collections import AsyncCollectionsResource
|
|
16
|
+
from fluxvector.resources.embeddings import AsyncEmbeddingsResource
|
|
17
|
+
from fluxvector.resources.search import AsyncSearchResource
|
|
18
|
+
from fluxvector.resources.usage import AsyncUsageResource
|
|
19
|
+
from fluxvector.resources.vectors import AsyncVectorsResource
|
|
20
|
+
from fluxvector.types import SearchResponse
|
|
21
|
+
|
|
22
|
+
_DEFAULT_BASE_URL = "https://fluxvector.dev"
|
|
23
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
24
|
+
_MAX_RETRIES = 3
|
|
25
|
+
_RETRY_STATUSES = {429, 500, 502, 503, 504}
|
|
26
|
+
_BACKOFF_BASE = 0.5
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AsyncFluxVector:
|
|
30
|
+
"""Asynchronous FluxVector API client.
|
|
31
|
+
|
|
32
|
+
Usage::
|
|
33
|
+
|
|
34
|
+
async with AsyncFluxVector(api_key="fv_live_abc123") as fv:
|
|
35
|
+
results = await fv.search("products", "comfortable shoes")
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
api_key: Optional[str] = None,
|
|
42
|
+
base_url: Optional[str] = None,
|
|
43
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
44
|
+
max_retries: int = _MAX_RETRIES,
|
|
45
|
+
) -> None:
|
|
46
|
+
self.api_key = api_key or os.environ.get("FLUXVECTOR_API_KEY", "")
|
|
47
|
+
if not self.api_key:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"API key is required. Pass api_key= or set FLUXVECTOR_API_KEY."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.base_url = (base_url or os.environ.get("FLUXVECTOR_BASE_URL", _DEFAULT_BASE_URL)).rstrip("/")
|
|
53
|
+
self.max_retries = max_retries
|
|
54
|
+
|
|
55
|
+
self._client = httpx.AsyncClient(
|
|
56
|
+
base_url=self.base_url,
|
|
57
|
+
timeout=timeout,
|
|
58
|
+
headers={
|
|
59
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"User-Agent": f"fluxvector-python/{__version__}",
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Resources
|
|
66
|
+
self.collections = AsyncCollectionsResource(self)
|
|
67
|
+
self.vectors = AsyncVectorsResource(self)
|
|
68
|
+
self.search = AsyncSearchResource(self)
|
|
69
|
+
self.embeddings = AsyncEmbeddingsResource(self)
|
|
70
|
+
self.api_keys = AsyncApiKeysResource(self)
|
|
71
|
+
self.usage = AsyncUsageResource(self)
|
|
72
|
+
|
|
73
|
+
def __repr__(self) -> str:
|
|
74
|
+
return f"AsyncFluxVector(base_url={self.base_url!r})"
|
|
75
|
+
|
|
76
|
+
async def __aenter__(self) -> AsyncFluxVector:
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
80
|
+
await self.close()
|
|
81
|
+
|
|
82
|
+
async def close(self) -> None:
|
|
83
|
+
"""Close the underlying HTTP connection pool."""
|
|
84
|
+
await self._client.aclose()
|
|
85
|
+
|
|
86
|
+
# ── Internal HTTP methods ────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
async def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
89
|
+
"""Make an HTTP request with retry logic."""
|
|
90
|
+
last_exc: Optional[Exception] = None
|
|
91
|
+
for attempt in range(self.max_retries + 1):
|
|
92
|
+
try:
|
|
93
|
+
response = await self._client.request(method, path, **kwargs)
|
|
94
|
+
if response.status_code in _RETRY_STATUSES and attempt < self.max_retries:
|
|
95
|
+
await self._backoff(attempt, response.headers)
|
|
96
|
+
continue
|
|
97
|
+
body = response.json() if response.content else {}
|
|
98
|
+
raise_for_status(response.status_code, body, dict(response.headers))
|
|
99
|
+
return body
|
|
100
|
+
except httpx.HTTPStatusError as exc:
|
|
101
|
+
last_exc = exc
|
|
102
|
+
if attempt < self.max_retries:
|
|
103
|
+
await self._backoff(attempt)
|
|
104
|
+
continue
|
|
105
|
+
raise
|
|
106
|
+
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
107
|
+
last_exc = exc
|
|
108
|
+
if attempt < self.max_retries:
|
|
109
|
+
await self._backoff(attempt)
|
|
110
|
+
continue
|
|
111
|
+
raise FVConnectionError(
|
|
112
|
+
f"Failed to connect to {self.base_url}: {exc}"
|
|
113
|
+
) from exc
|
|
114
|
+
raise last_exc # type: ignore[misc]
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
async def _backoff(attempt: int, headers: Optional[Any] = None) -> None:
|
|
118
|
+
"""Exponential backoff with optional Retry-After header support."""
|
|
119
|
+
if headers and hasattr(headers, "get"):
|
|
120
|
+
retry_after = headers.get("Retry-After")
|
|
121
|
+
if retry_after:
|
|
122
|
+
try:
|
|
123
|
+
await asyncio.sleep(float(retry_after))
|
|
124
|
+
return
|
|
125
|
+
except (ValueError, TypeError):
|
|
126
|
+
pass
|
|
127
|
+
await asyncio.sleep(_BACKOFF_BASE * (2 ** attempt))
|
|
128
|
+
|
|
129
|
+
async def _get(self, path: str, *, params: Optional[dict] = None) -> Any:
|
|
130
|
+
return await self._request("GET", path, params=params)
|
|
131
|
+
|
|
132
|
+
async def _post(self, path: str, *, json: Optional[dict] = None) -> Any:
|
|
133
|
+
return await self._request("POST", path, json=json)
|
|
134
|
+
|
|
135
|
+
async def _patch(self, path: str, *, json: Optional[dict] = None) -> Any:
|
|
136
|
+
return await self._request("PATCH", path, json=json)
|
|
137
|
+
|
|
138
|
+
async def _delete(self, path: str) -> Any:
|
|
139
|
+
return await self._request("DELETE", path)
|