vectorizer-sdk 3.0.0__tar.gz → 3.2.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.
Files changed (51) hide show
  1. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/PKG-INFO +55 -6
  2. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/README.md +54 -5
  3. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/pyproject.toml +110 -110
  4. vectorizer_sdk-3.2.0/tests/test_retry_after_parse.py +51 -0
  5. vectorizer_sdk-3.2.0/utils/http_client.py +212 -0
  6. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer/_base.py +19 -1
  7. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer/auth.py +27 -1
  8. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer/client.py +10 -1
  9. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer/vectors.py +47 -0
  10. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer_sdk.egg-info/PKG-INFO +55 -6
  11. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer_sdk.egg-info/SOURCES.txt +1 -0
  12. vectorizer_sdk-3.0.0/utils/http_client.py +0 -145
  13. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/LICENSE +0 -0
  14. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/rpc/__init__.py +0 -0
  15. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/rpc/_codec.py +0 -0
  16. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/rpc/async_client.py +0 -0
  17. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/rpc/commands.py +0 -0
  18. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/rpc/endpoint.py +0 -0
  19. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/rpc/pool.py +0 -0
  20. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/rpc/sync_client.py +0 -0
  21. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/rpc/types.py +0 -0
  22. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/setup.cfg +0 -0
  23. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_client_integration.py +0 -0
  24. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_discovery.py +0 -0
  25. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_exceptions.py +0 -0
  26. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_file_operations.py +0 -0
  27. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_file_upload.py +0 -0
  28. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_graph.py +0 -0
  29. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_http_client.py +0 -0
  30. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_intelligent_search.py +0 -0
  31. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_mock_transport.py +0 -0
  32. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_models.py +0 -0
  33. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_qdrant_advanced.py +0 -0
  34. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_routing.py +0 -0
  35. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_sdk_comprehensive.py +0 -0
  36. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_simple.py +0 -0
  37. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_umicp.py +0 -0
  38. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/tests/test_validation.py +0 -0
  39. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/utils/__init__.py +0 -0
  40. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/utils/transport.py +0 -0
  41. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/utils/umicp_client.py +0 -0
  42. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/utils/validation.py +0 -0
  43. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer/__init__.py +0 -0
  44. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer/admin.py +0 -0
  45. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer/collections.py +0 -0
  46. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer/graph.py +0 -0
  47. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer/search.py +0 -0
  48. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer_sdk.egg-info/dependency_links.txt +0 -0
  49. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer_sdk.egg-info/entry_points.txt +0 -0
  50. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer_sdk.egg-info/requires.txt +0 -0
  51. {vectorizer_sdk-3.0.0 → vectorizer_sdk-3.2.0}/vectorizer_sdk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vectorizer_sdk
3
- Version: 3.0.0
3
+ Version: 3.2.0
4
4
  Summary: Python SDK for Vectorizer - Semantic search, vector operations, and the VectorizerRPC binary transport (default in v3.x)
5
5
  Author-email: HiveLLM Team <team@hivellm.org>
6
6
  License: Apache-2.0
@@ -58,9 +58,38 @@ Dynamic: license-file
58
58
  A comprehensive Python SDK for the Vectorizer semantic search service.
59
59
 
60
60
  **Package**: `vectorizer_sdk` (PEP 625 compliant)
61
- **Version**: 3.0.0
61
+ **Version**: 3.2.0
62
62
  **PyPI**: https://pypi.org/project/vectorizer-sdk/
63
63
 
64
+ ## v3.2 — backpressure-aware client (HTTP 429 + `Retry-After`)
65
+
66
+ The REST `VectorizerClient` honors server-side bulk-upsert
67
+ backpressure shipped in Vectorizer 3.2.0
68
+ ([#263](https://github.com/hivellm/vectorizer/issues/263)). On HTTP
69
+ `429 Too Many Requests` the client parses `Retry-After` (seconds
70
+ form, 1 s default, 30 s cap), sleeps, and retries up to 3 times
71
+ before raising a typed `RateLimitError`. Pre-3.2.0 clients bounced
72
+ 429s into a generic 5xx and lost the retry budget. Identical
73
+ semantics ship in every first-party SDK (Rust, Python, TypeScript,
74
+ Go, C#) — see `tests/test_retry_after_parse.py`.
75
+
76
+ ## v3.1 — `/insert_vectors` + stable client-id upserts
77
+
78
+ - `insert_vectors(collection, vectors, public_key=None)` — bulk-
79
+ insert pre-computed embeddings with caller-supplied vector ids.
80
+ Skips the embedding pipeline entirely.
81
+ - `insert` / `insert_texts`: the request `id` is now used verbatim
82
+ as the stored `Vector.id` (non-chunked) or as `<id>#<chunk_index>`
83
+ (chunked). Re-running the same payload upserts in place instead
84
+ of duplicating.
85
+ - Chunked vectors expose a flat payload layout (`{content,
86
+ file_path, chunk_index, parent_id, ...user_metadata}`). Legacy
87
+ nested payloads from ≤ 3.0.x stay readable during the deprecation
88
+ window.
89
+
90
+ Client-id contract: non-empty, length ≤ 256, no leading/trailing
91
+ whitespace, must not contain `#`.
92
+
64
93
  ## v3.0 — VectorizerRPC is the default transport
65
94
 
66
95
  Starting with v3.0, the recommended transport is **VectorizerRPC**: a
@@ -79,6 +108,8 @@ import vectorizer_sdk
79
108
 
80
109
  async def main():
81
110
  client = await vectorizer_sdk.connect_async("vectorizer://127.0.0.1:15503")
111
+ # `hello` and `search_basic` are RPC-only (not available on the legacy
112
+ # REST `VectorizerClient`).
82
113
  await client.hello(vectorizer_sdk.HelloPayload(client_name="my-app"))
83
114
  print(await client.list_collections())
84
115
  hits = await client.search_basic("docs", "vector database", limit=5)
@@ -140,7 +171,7 @@ for a runnable end-to-end example.
140
171
  pip install vectorizer-sdk
141
172
 
142
173
  # Or specific version
143
- pip install vectorizer-sdk==3.0.0
174
+ pip install vectorizer-sdk==3.2.0
144
175
  ```
145
176
 
146
177
  ## Package Layout (v3.x)
@@ -477,6 +508,11 @@ related = await client.get_related_files(
477
508
 
478
509
  ### Summarization Operations
479
510
 
511
+ > WARNING: The `/summarize/*` REST endpoints are documented but not yet wired
512
+ > server-side (see `DOC_GAP_ANALYSIS`). The SDK methods below
513
+ > (`summarize_text`, `summarize_context`) will fail until server wiring is
514
+ > complete.
515
+
480
516
  #### Summarize Text
481
517
  Summarize text using various methods:
482
518
 
@@ -509,6 +545,11 @@ summary = await client.summarize_context(
509
545
 
510
546
  ### Workspace Management
511
547
 
548
+ > WARNING: `add_workspace`, `list_workspaces`, and `remove_workspace` are
549
+ > exposed via the REST transport through dynamic `__getattr__` delegation,
550
+ > but they are not first-class SDK methods yet. They work at runtime but
551
+ > won't autocomplete in IDEs. A future release will add explicit methods.
552
+
512
553
  #### Add Workspace
513
554
  Add a new workspace:
514
555
 
@@ -537,6 +578,11 @@ await client.remove_workspace(
537
578
 
538
579
  ### Backup Operations
539
580
 
581
+ > WARNING: `create_backup`, `list_backups`, and `restore_backup` are
582
+ > exposed via the REST transport through dynamic `__getattr__` delegation,
583
+ > but they are not first-class SDK methods yet. They work at runtime but
584
+ > won't autocomplete in IDEs. A future release will add explicit methods.
585
+
540
586
  #### Create Backup
541
587
  Create a backup of collections:
542
588
 
@@ -664,8 +710,11 @@ await client.create_collection("documents", dimension=768)
664
710
  await client.insert_texts("documents", [
665
711
  {"id": "doc1", "text": "Sample document", "metadata": {"source": "api"}}
666
712
  ])
667
- await client.update_vector("documents", "doc1", metadata={"updated": True})
668
- await client.delete_vector("documents", "doc1")
713
+ # Update-via-reinsert: re-call `insert_texts` with the same id to replace the record.
714
+ await client.insert_texts("documents", [
715
+ {"id": "doc1", "text": "Sample document (updated)", "metadata": {"updated": True}}
716
+ ])
717
+ await client.delete_vectors("documents", ["doc1"])
669
718
 
670
719
  # Reads automatically go to replicas (load balanced)
671
720
  results = await client.search_vectors("documents", query="sample", limit=10)
@@ -702,7 +751,7 @@ The SDK automatically classifies operations:
702
751
 
703
752
  | Operation Type | Routed To | Methods |
704
753
  |---------------|-----------|---------|
705
- | **Writes** | Always Master | `insert_texts`, `insert_vectors`, `update_vector`, `delete_vector`, `create_collection`, `delete_collection` |
754
+ | **Writes** | Always Master | `insert_texts`, `insert_vectors`, `delete_vectors`, `create_collection`, `delete_collection` |
706
755
  | **Reads** | Based on `read_preference` | `search_vectors`, `get_vector`, `list_collections`, `intelligent_search`, `semantic_search`, `hybrid_search` |
707
756
 
708
757
  #### Standalone Mode (Single Node)
@@ -7,9 +7,38 @@
7
7
  A comprehensive Python SDK for the Vectorizer semantic search service.
8
8
 
9
9
  **Package**: `vectorizer_sdk` (PEP 625 compliant)
10
- **Version**: 3.0.0
10
+ **Version**: 3.2.0
11
11
  **PyPI**: https://pypi.org/project/vectorizer-sdk/
12
12
 
13
+ ## v3.2 — backpressure-aware client (HTTP 429 + `Retry-After`)
14
+
15
+ The REST `VectorizerClient` honors server-side bulk-upsert
16
+ backpressure shipped in Vectorizer 3.2.0
17
+ ([#263](https://github.com/hivellm/vectorizer/issues/263)). On HTTP
18
+ `429 Too Many Requests` the client parses `Retry-After` (seconds
19
+ form, 1 s default, 30 s cap), sleeps, and retries up to 3 times
20
+ before raising a typed `RateLimitError`. Pre-3.2.0 clients bounced
21
+ 429s into a generic 5xx and lost the retry budget. Identical
22
+ semantics ship in every first-party SDK (Rust, Python, TypeScript,
23
+ Go, C#) — see `tests/test_retry_after_parse.py`.
24
+
25
+ ## v3.1 — `/insert_vectors` + stable client-id upserts
26
+
27
+ - `insert_vectors(collection, vectors, public_key=None)` — bulk-
28
+ insert pre-computed embeddings with caller-supplied vector ids.
29
+ Skips the embedding pipeline entirely.
30
+ - `insert` / `insert_texts`: the request `id` is now used verbatim
31
+ as the stored `Vector.id` (non-chunked) or as `<id>#<chunk_index>`
32
+ (chunked). Re-running the same payload upserts in place instead
33
+ of duplicating.
34
+ - Chunked vectors expose a flat payload layout (`{content,
35
+ file_path, chunk_index, parent_id, ...user_metadata}`). Legacy
36
+ nested payloads from ≤ 3.0.x stay readable during the deprecation
37
+ window.
38
+
39
+ Client-id contract: non-empty, length ≤ 256, no leading/trailing
40
+ whitespace, must not contain `#`.
41
+
13
42
  ## v3.0 — VectorizerRPC is the default transport
14
43
 
15
44
  Starting with v3.0, the recommended transport is **VectorizerRPC**: a
@@ -28,6 +57,8 @@ import vectorizer_sdk
28
57
 
29
58
  async def main():
30
59
  client = await vectorizer_sdk.connect_async("vectorizer://127.0.0.1:15503")
60
+ # `hello` and `search_basic` are RPC-only (not available on the legacy
61
+ # REST `VectorizerClient`).
31
62
  await client.hello(vectorizer_sdk.HelloPayload(client_name="my-app"))
32
63
  print(await client.list_collections())
33
64
  hits = await client.search_basic("docs", "vector database", limit=5)
@@ -89,7 +120,7 @@ for a runnable end-to-end example.
89
120
  pip install vectorizer-sdk
90
121
 
91
122
  # Or specific version
92
- pip install vectorizer-sdk==3.0.0
123
+ pip install vectorizer-sdk==3.2.0
93
124
  ```
94
125
 
95
126
  ## Package Layout (v3.x)
@@ -426,6 +457,11 @@ related = await client.get_related_files(
426
457
 
427
458
  ### Summarization Operations
428
459
 
460
+ > WARNING: The `/summarize/*` REST endpoints are documented but not yet wired
461
+ > server-side (see `DOC_GAP_ANALYSIS`). The SDK methods below
462
+ > (`summarize_text`, `summarize_context`) will fail until server wiring is
463
+ > complete.
464
+
429
465
  #### Summarize Text
430
466
  Summarize text using various methods:
431
467
 
@@ -458,6 +494,11 @@ summary = await client.summarize_context(
458
494
 
459
495
  ### Workspace Management
460
496
 
497
+ > WARNING: `add_workspace`, `list_workspaces`, and `remove_workspace` are
498
+ > exposed via the REST transport through dynamic `__getattr__` delegation,
499
+ > but they are not first-class SDK methods yet. They work at runtime but
500
+ > won't autocomplete in IDEs. A future release will add explicit methods.
501
+
461
502
  #### Add Workspace
462
503
  Add a new workspace:
463
504
 
@@ -486,6 +527,11 @@ await client.remove_workspace(
486
527
 
487
528
  ### Backup Operations
488
529
 
530
+ > WARNING: `create_backup`, `list_backups`, and `restore_backup` are
531
+ > exposed via the REST transport through dynamic `__getattr__` delegation,
532
+ > but they are not first-class SDK methods yet. They work at runtime but
533
+ > won't autocomplete in IDEs. A future release will add explicit methods.
534
+
489
535
  #### Create Backup
490
536
  Create a backup of collections:
491
537
 
@@ -613,8 +659,11 @@ await client.create_collection("documents", dimension=768)
613
659
  await client.insert_texts("documents", [
614
660
  {"id": "doc1", "text": "Sample document", "metadata": {"source": "api"}}
615
661
  ])
616
- await client.update_vector("documents", "doc1", metadata={"updated": True})
617
- await client.delete_vector("documents", "doc1")
662
+ # Update-via-reinsert: re-call `insert_texts` with the same id to replace the record.
663
+ await client.insert_texts("documents", [
664
+ {"id": "doc1", "text": "Sample document (updated)", "metadata": {"updated": True}}
665
+ ])
666
+ await client.delete_vectors("documents", ["doc1"])
618
667
 
619
668
  # Reads automatically go to replicas (load balanced)
620
669
  results = await client.search_vectors("documents", query="sample", limit=10)
@@ -651,7 +700,7 @@ The SDK automatically classifies operations:
651
700
 
652
701
  | Operation Type | Routed To | Methods |
653
702
  |---------------|-----------|---------|
654
- | **Writes** | Always Master | `insert_texts`, `insert_vectors`, `update_vector`, `delete_vector`, `create_collection`, `delete_collection` |
703
+ | **Writes** | Always Master | `insert_texts`, `insert_vectors`, `delete_vectors`, `create_collection`, `delete_collection` |
655
704
  | **Reads** | Based on `read_preference` | `search_vectors`, `get_vector`, `list_collections`, `intelligent_search`, `semantic_search`, `hybrid_search` |
656
705
 
657
706
  #### Standalone Mode (Single Node)
@@ -1,110 +1,110 @@
1
- [build-system]
2
- requires = ["setuptools>=61.0", "wheel"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "vectorizer_sdk"
7
- version = "3.0.0"
8
- description = "Python SDK for Vectorizer - Semantic search, vector operations, and the VectorizerRPC binary transport (default in v3.x)"
9
- readme = "README.md"
10
- requires-python = ">=3.8"
11
- license = {text = "Apache-2.0"}
12
- authors = [
13
- {name = "HiveLLM Team", email = "team@hivellm.org"}
14
- ]
15
- keywords = [
16
- "vectorizer",
17
- "semantic-search",
18
- "embeddings",
19
- "machine-learning",
20
- "ai",
21
- "search",
22
- "vectors",
23
- "similarity",
24
- "hivellm",
25
- "umicp"
26
- ]
27
- classifiers = [
28
- "Development Status :: 5 - Production/Stable",
29
- "Intended Audience :: Developers",
30
- "Operating System :: OS Independent",
31
- "Programming Language :: Python :: 3",
32
- "Programming Language :: Python :: 3.8",
33
- "Programming Language :: Python :: 3.9",
34
- "Programming Language :: Python :: 3.10",
35
- "Programming Language :: Python :: 3.11",
36
- "Programming Language :: Python :: 3.12",
37
- "Topic :: Software Development :: Libraries :: Python Modules",
38
- "Topic :: Scientific/Engineering :: Artificial Intelligence",
39
- "Topic :: Text Processing :: Indexing",
40
- "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
41
- ]
42
-
43
- dependencies = [
44
- "aiohttp>=3.10.0",
45
- "msgpack>=1.1.0",
46
- ]
47
-
48
- [project.optional-dependencies]
49
- umicp = [
50
- "umicp-python>=0.1.3",
51
- ]
52
- dev = [
53
- "pytest>=8.0.0",
54
- "pytest-asyncio>=0.24.0",
55
- "pytest-cov>=5.0.0",
56
- "black>=25.0.0",
57
- "isort>=5.13.0",
58
- "flake8>=7.0.0",
59
- "mypy>=1.11.0",
60
- "pre-commit>=4.0.0",
61
- ]
62
- docs = [
63
- "sphinx>=7.0.0",
64
- "sphinx-rtd-theme>=2.0.0",
65
- "myst-parser>=3.0.0",
66
- ]
67
- test = [
68
- "pytest>=8.0.0",
69
- "pytest-asyncio>=0.24.0",
70
- "pytest-cov>=5.0.0",
71
- "httpx>=0.27.0",
72
- ]
73
-
74
- [project.urls]
75
- Homepage = "https://github.com/hivellm/vectorizer"
76
- Documentation = "https://github.com/hivellm/vectorizer/tree/main/docs"
77
- Repository = "https://github.com/hivellm/vectorizer"
78
- Issues = "https://github.com/hivellm/vectorizer/issues"
79
-
80
- [project.scripts]
81
- vectorizer-cli = "cli:main"
82
-
83
- [tool.setuptools.packages.find]
84
- include = ["*"]
85
- exclude = ["tests*", "examples*", "docs*"]
86
-
87
- [tool.setuptools.package-data]
88
- "*" = ["py.typed"]
89
-
90
- [tool.pytest.ini_options]
91
- asyncio_mode = "auto"
92
- testpaths = ["tests"]
93
- python_files = ["test_*.py"]
94
- python_classes = ["Test*"]
95
- python_functions = ["test_*"]
96
-
97
- [tool.black]
98
- line-length = 100
99
- target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
100
-
101
- [tool.isort]
102
- profile = "black"
103
- line_length = 100
104
-
105
- [tool.mypy]
106
- python_version = "3.8"
107
- warn_return_any = true
108
- warn_unused_configs = true
109
- disallow_untyped_defs = false
110
-
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vectorizer_sdk"
7
+ version = "3.2.0"
8
+ description = "Python SDK for Vectorizer - Semantic search, vector operations, and the VectorizerRPC binary transport (default in v3.x)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "Apache-2.0"}
12
+ authors = [
13
+ {name = "HiveLLM Team", email = "team@hivellm.org"}
14
+ ]
15
+ keywords = [
16
+ "vectorizer",
17
+ "semantic-search",
18
+ "embeddings",
19
+ "machine-learning",
20
+ "ai",
21
+ "search",
22
+ "vectors",
23
+ "similarity",
24
+ "hivellm",
25
+ "umicp"
26
+ ]
27
+ classifiers = [
28
+ "Development Status :: 5 - Production/Stable",
29
+ "Intended Audience :: Developers",
30
+ "Operating System :: OS Independent",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.8",
33
+ "Programming Language :: Python :: 3.9",
34
+ "Programming Language :: Python :: 3.10",
35
+ "Programming Language :: Python :: 3.11",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Topic :: Software Development :: Libraries :: Python Modules",
38
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
39
+ "Topic :: Text Processing :: Indexing",
40
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
41
+ ]
42
+
43
+ dependencies = [
44
+ "aiohttp>=3.10.0",
45
+ "msgpack>=1.1.0",
46
+ ]
47
+
48
+ [project.optional-dependencies]
49
+ umicp = [
50
+ "umicp-python>=0.1.3",
51
+ ]
52
+ dev = [
53
+ "pytest>=8.0.0",
54
+ "pytest-asyncio>=0.24.0",
55
+ "pytest-cov>=5.0.0",
56
+ "black>=25.0.0",
57
+ "isort>=5.13.0",
58
+ "flake8>=7.0.0",
59
+ "mypy>=1.11.0",
60
+ "pre-commit>=4.0.0",
61
+ ]
62
+ docs = [
63
+ "sphinx>=7.0.0",
64
+ "sphinx-rtd-theme>=2.0.0",
65
+ "myst-parser>=3.0.0",
66
+ ]
67
+ test = [
68
+ "pytest>=8.0.0",
69
+ "pytest-asyncio>=0.24.0",
70
+ "pytest-cov>=5.0.0",
71
+ "httpx>=0.27.0",
72
+ ]
73
+
74
+ [project.urls]
75
+ Homepage = "https://github.com/hivellm/vectorizer"
76
+ Documentation = "https://github.com/hivellm/vectorizer/tree/main/docs"
77
+ Repository = "https://github.com/hivellm/vectorizer"
78
+ Issues = "https://github.com/hivellm/vectorizer/issues"
79
+
80
+ [project.scripts]
81
+ vectorizer-cli = "cli:main"
82
+
83
+ [tool.setuptools.packages.find]
84
+ include = ["*"]
85
+ exclude = ["tests*", "examples*", "docs*"]
86
+
87
+ [tool.setuptools.package-data]
88
+ "*" = ["py.typed"]
89
+
90
+ [tool.pytest.ini_options]
91
+ asyncio_mode = "auto"
92
+ testpaths = ["tests"]
93
+ python_files = ["test_*.py"]
94
+ python_classes = ["Test*"]
95
+ python_functions = ["test_*"]
96
+
97
+ [tool.black]
98
+ line-length = 100
99
+ target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
100
+
101
+ [tool.isort]
102
+ profile = "black"
103
+ line_length = 100
104
+
105
+ [tool.mypy]
106
+ python_version = "3.8"
107
+ warn_return_any = true
108
+ warn_unused_configs = true
109
+ disallow_untyped_defs = false
110
+
@@ -0,0 +1,51 @@
1
+ """Retry-After header parser tests for the Python SDK (issue #263, phase9 §7).
2
+
3
+ The full retry loop is exercised end-to-end at the server level by
4
+ ``crates/vectorizer-server/tests/backpressure_429.rs``; here we only
5
+ lock in the value-parsing edges that determine how aggressively the
6
+ SDK backs off.
7
+
8
+ These constants are kept in sync with ``utils/http_client.py``:
9
+
10
+ - missing/unparseable header -> 1 s default
11
+ - ``Retry-After: 0`` -> 1 s default (never busy-loop)
12
+ - capped at 30 s so a misconfigured server cannot pin the client
13
+ """
14
+
15
+ import os
16
+ import sys
17
+ import unittest
18
+
19
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
20
+
21
+ from utils.http_client import _parse_retry_after # type: ignore[import-not-found]
22
+
23
+
24
+ class TestRetryAfterParse(unittest.TestCase):
25
+ def test_missing_header_returns_default(self) -> None:
26
+ self.assertEqual(_parse_retry_after(None), 1)
27
+
28
+ def test_empty_or_whitespace_returns_default(self) -> None:
29
+ self.assertEqual(_parse_retry_after(""), 1)
30
+ self.assertEqual(_parse_retry_after(" "), 1)
31
+
32
+ def test_zero_returns_default_to_avoid_busy_loop(self) -> None:
33
+ self.assertEqual(_parse_retry_after("0"), 1)
34
+
35
+ def test_unparseable_string_returns_default(self) -> None:
36
+ self.assertEqual(_parse_retry_after("not-a-number"), 1)
37
+
38
+ def test_small_values_pass_through_verbatim(self) -> None:
39
+ self.assertEqual(_parse_retry_after("3"), 3)
40
+ self.assertEqual(_parse_retry_after("7"), 7)
41
+ self.assertEqual(_parse_retry_after(" 5 "), 5)
42
+
43
+ def test_large_values_are_capped_at_30s(self) -> None:
44
+ # If this assertion ever flips, audit _RETRY_AFTER_MAX_SECONDS
45
+ # in utils/http_client.py first.
46
+ self.assertEqual(_parse_retry_after("3600"), 30)
47
+ self.assertEqual(_parse_retry_after("31"), 30)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ unittest.main()
@@ -0,0 +1,212 @@
1
+ """
2
+ HTTP client utility for making API requests using aiohttp.
3
+ """
4
+
5
+ from typing import Optional, Dict, Any
6
+ import aiohttp
7
+ import asyncio
8
+ import logging
9
+
10
+ try:
11
+ from ..exceptions import (
12
+ NetworkError,
13
+ ServerError,
14
+ AuthenticationError,
15
+ RateLimitError,
16
+ )
17
+ except ImportError:
18
+ from exceptions import (
19
+ NetworkError,
20
+ ServerError,
21
+ AuthenticationError,
22
+ RateLimitError,
23
+ )
24
+
25
+ # Issue #263: cap Retry-After respect at this many seconds so a
26
+ # misconfigured server can't pin a client into a half-hour sleep.
27
+ _RETRY_AFTER_MAX_SECONDS = 30
28
+ # Floor for parsed Retry-After so a `0` or missing header still yields
29
+ # a noticeable backoff rather than busy-looping the server.
30
+ _RETRY_AFTER_DEFAULT_SECONDS = 1
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class HTTPClient:
36
+ """HTTP transport client."""
37
+
38
+ def __init__(
39
+ self,
40
+ base_url: str = "http://localhost:15002",
41
+ api_key: Optional[str] = None,
42
+ timeout: int = 30,
43
+ max_retries: int = 3
44
+ ):
45
+ """
46
+ Initialize HTTP client.
47
+
48
+ Args:
49
+ base_url: Base URL for HTTP API
50
+ api_key: API key for authentication
51
+ timeout: Request timeout in seconds
52
+ max_retries: Maximum number of retry attempts
53
+ """
54
+ self.base_url = base_url.rstrip('/')
55
+ self.api_key = api_key
56
+ self.timeout = timeout
57
+ self.max_retries = max_retries
58
+ self._session: Optional[aiohttp.ClientSession] = None
59
+
60
+ async def _ensure_session(self):
61
+ """Ensure aiohttp session is created."""
62
+ if self._session is None or self._session.closed:
63
+ headers = {"Content-Type": "application/json"}
64
+ if self.api_key:
65
+ # JWT shape → `Authorization: Bearer`; anything else
66
+ # (raw API keys from `POST /auth/keys`) → `X-API-Key`.
67
+ # Server middleware won't fall back Bearer → api_key,
68
+ # so routing has to happen client-side.
69
+ parts = self.api_key.split(".")
70
+ if len(parts) == 3 and all(p for p in parts):
71
+ headers["Authorization"] = f"Bearer {self.api_key}"
72
+ else:
73
+ headers["X-API-Key"] = self.api_key
74
+
75
+ timeout_config = aiohttp.ClientTimeout(total=self.timeout)
76
+ self._session = aiohttp.ClientSession(
77
+ headers=headers,
78
+ timeout=timeout_config
79
+ )
80
+
81
+ async def close(self):
82
+ """Close the HTTP session."""
83
+ if self._session and not self._session.closed:
84
+ await self._session.close()
85
+ self._session = None
86
+
87
+ async def request(
88
+ self,
89
+ method: str,
90
+ path: str,
91
+ data: Optional[Dict[str, Any]] = None
92
+ ) -> Any:
93
+ """
94
+ Make an HTTP request.
95
+
96
+ Honors `Retry-After` on 429 responses (issue #263): the client
97
+ sleeps for the header's value (capped) and retries up to
98
+ ``max_retries`` times. After exhaustion a ``RateLimitError`` is
99
+ raised so callers can surface the back-pressure to the user.
100
+
101
+ Args:
102
+ method: HTTP method
103
+ path: API endpoint path
104
+ data: Request data
105
+
106
+ Returns:
107
+ Response data
108
+ """
109
+ await self._ensure_session()
110
+
111
+ url = f"{self.base_url}{path}"
112
+ attempts_remaining = self.max_retries
113
+ last_429_text: Optional[str] = None
114
+
115
+ while True:
116
+ try:
117
+ async with self._session.request(
118
+ method,
119
+ url,
120
+ json=data if data else None,
121
+ ) as response:
122
+ if response.status == 429:
123
+ last_429_text = await response.text()
124
+ if attempts_remaining <= 0:
125
+ raise RateLimitError(
126
+ f"HTTP 429 after {self.max_retries} retries: "
127
+ f"{last_429_text}"
128
+ )
129
+ delay = _parse_retry_after(
130
+ response.headers.get("Retry-After")
131
+ )
132
+ logger.info(
133
+ "Vectorizer 429 — sleeping %.1fs before retry "
134
+ "(remaining attempts=%d)",
135
+ delay,
136
+ attempts_remaining,
137
+ )
138
+ attempts_remaining -= 1
139
+ await asyncio.sleep(delay)
140
+ continue
141
+
142
+ if response.status >= 400:
143
+ error_text = await response.text()
144
+ raise self._handle_error(response.status, error_text)
145
+
146
+ content_type = response.headers.get('Content-Type', '')
147
+ if 'application/json' in content_type:
148
+ return await response.json()
149
+ return await response.text()
150
+
151
+ except (ServerError, AuthenticationError, RateLimitError):
152
+ raise
153
+ except aiohttp.ClientError as e:
154
+ raise NetworkError(f"HTTP request failed: {e}")
155
+ except asyncio.TimeoutError:
156
+ raise NetworkError("Request timeout")
157
+ except Exception as e:
158
+ raise NetworkError(f"Unknown error: {e}")
159
+
160
+ async def get(self, path: str) -> Any:
161
+ """Make a GET request."""
162
+ return await self.request("GET", path)
163
+
164
+ async def post(self, path: str, data: Optional[Dict[str, Any]] = None) -> Any:
165
+ """Make a POST request."""
166
+ return await self.request("POST", path, data)
167
+
168
+ async def put(self, path: str, data: Optional[Dict[str, Any]] = None) -> Any:
169
+ """Make a PUT request."""
170
+ return await self.request("PUT", path, data)
171
+
172
+ async def delete(self, path: str) -> Any:
173
+ """Make a DELETE request."""
174
+ return await self.request("DELETE", path)
175
+
176
+ def _handle_error(self, status: int, error_text: str) -> Exception:
177
+ """Handle HTTP errors and convert to appropriate exceptions."""
178
+ message = f"HTTP {status}: {error_text}"
179
+
180
+ if status == 401:
181
+ return AuthenticationError(message)
182
+ elif status == 403:
183
+ return AuthenticationError("Access forbidden")
184
+ elif status == 404:
185
+ return ServerError("Resource not found")
186
+ elif status == 429:
187
+ # 429 is handled in `request()` via Retry-After; reaching
188
+ # here means the caller bypassed retry handling, so
189
+ # surface a typed RateLimitError instead of a generic 5xx.
190
+ return RateLimitError(message)
191
+ elif status in (500, 502, 503, 504):
192
+ return ServerError(message)
193
+ else:
194
+ return ServerError(message)
195
+
196
+
197
+ def _parse_retry_after(value: Optional[str]) -> float:
198
+ """Parse a ``Retry-After`` header value (seconds form only).
199
+
200
+ Returns a sane default + caps an unreasonably large server hint
201
+ so a misconfigured server can't pin a client into a long sleep.
202
+ """
203
+ if not value:
204
+ return _RETRY_AFTER_DEFAULT_SECONDS
205
+ try:
206
+ seconds = float(value.strip())
207
+ except ValueError:
208
+ return _RETRY_AFTER_DEFAULT_SECONDS
209
+ if seconds <= 0:
210
+ return _RETRY_AFTER_DEFAULT_SECONDS
211
+ return min(seconds, _RETRY_AFTER_MAX_SECONDS)
212
+
@@ -112,6 +112,22 @@ class RestTransport(Transport):
112
112
  await self._http.close()
113
113
 
114
114
 
115
+ def _looks_like_jwt(token: str) -> bool:
116
+ """Cheap JWT shape sniff: three non-empty base64url-encoded segments
117
+ joined by ``.``. Raw Vectorizer API keys (from ``POST /auth/keys``)
118
+ are a single alphanumeric string and fail this check, so they get
119
+ routed to ``X-API-Key`` rather than ``Authorization: Bearer``.
120
+
121
+ The server's auth middleware treats every ``Bearer`` string as a
122
+ JWT and never falls back to the API-key validator, so sending a
123
+ raw key under ``Bearer`` silently 401s. This sniff keeps the public
124
+ ``api_key`` config field unchanged while routing each credential
125
+ down the header the server actually accepts.
126
+ """
127
+ parts = token.split(".")
128
+ return len(parts) == 3 and all(p for p in parts)
129
+
130
+
115
131
  @dataclass
116
132
  class AuthState:
117
133
  """Bearer-token + API-key state shared across surface clients."""
@@ -121,7 +137,9 @@ class AuthState:
121
137
  def headers(self) -> dict:
122
138
  if not self.api_key:
123
139
  return {}
124
- return {"Authorization": f"Bearer {self.api_key}"}
140
+ if _looks_like_jwt(self.api_key):
141
+ return {"Authorization": f"Bearer {self.api_key}"}
142
+ return {"X-API-Key": self.api_key}
125
143
 
126
144
 
127
145
  @dataclass
@@ -22,7 +22,8 @@ class AuthClient(_ApiBase):
22
22
 
23
23
  from vectorizer import RestTransport, AuthClient
24
24
  auth = AuthClient(RestTransport("http://localhost:15002"), api_key="sk-...")
25
- print(auth.headers()) # -> {"Authorization": "Bearer sk-..."}
25
+ print(auth.headers()) # raw API key → {"X-API-Key": "sk-..."}
26
+ # # JWT → {"Authorization": "Bearer <jwt>"}
26
27
 
27
28
  In the :class:`vectorizer.VectorizerClient` facade this is the
28
29
  surface that owns the API key and surfaces it via :attr:`api_key`
@@ -47,3 +48,28 @@ class AuthClient(_ApiBase):
47
48
  def set_api_key(self, api_key: Optional[str]) -> None:
48
49
  """Replace the current API key — affects future request headers."""
49
50
  self._auth.api_key = api_key
51
+
52
+ async def login(self, username: str, password: str) -> str:
53
+ """Exchange ``(username, password)`` for a JWT via
54
+ ``POST /auth/login`` and return the raw ``access_token``.
55
+
56
+ The token is **not** stored on ``self`` — to use it for
57
+ subsequent requests, call :meth:`set_api_key` with the token
58
+ (``AuthState.headers`` sniffs the JWT shape and routes it onto
59
+ ``Authorization: Bearer …`` automatically).
60
+
61
+ When the server runs with ``auth.enabled: false`` this
62
+ endpoint returns 404 — dev servers without auth don't need
63
+ to call :meth:`login` at all.
64
+ """
65
+ data = await self._transport.post(
66
+ "/auth/login",
67
+ {"username": username, "password": password},
68
+ )
69
+ token = data.get("access_token") if isinstance(data, dict) else None
70
+ if not isinstance(token, str) or not token:
71
+ raise ValueError(
72
+ "login response missing `access_token`; server payload: "
73
+ f"{data!r}"
74
+ )
75
+ return token
@@ -274,7 +274,16 @@ class VectorizerClient:
274
274
  if self._session is None or self._session.closed:
275
275
  headers = {}
276
276
  if self.api_key:
277
- headers["Authorization"] = f"Bearer {self.api_key}"
277
+ # Route JWTs to `Authorization: Bearer` and raw API
278
+ # keys to `X-API-Key`. Server middleware treats
279
+ # every Bearer string as a JWT and won't fall back
280
+ # to the API-key validator — sending a raw key as
281
+ # Bearer silently 401s on every gated route.
282
+ from ._base import _looks_like_jwt
283
+ if _looks_like_jwt(self.api_key):
284
+ headers["Authorization"] = f"Bearer {self.api_key}"
285
+ else:
286
+ headers["X-API-Key"] = self.api_key
278
287
 
279
288
  timeout = aiohttp.ClientTimeout(total=self.timeout)
280
289
  self._session = aiohttp.ClientSession(
@@ -112,6 +112,15 @@ class VectorsClient(_ApiBase):
112
112
  """
113
113
  Get a specific vector by ID.
114
114
 
115
+ .. warning::
116
+ **Server caveat (observed on ``hivehub/vectorizer:3.0.x``):**
117
+ this endpoint currently returns HTTP 200 with a synthetic
118
+ uniform-vector payload (``[0.1, 0.1, ...]``) even for ids
119
+ that don't exist. Callers that need real miss detection
120
+ should probe via :meth:`list_vectors` or search and not
121
+ trust a successful response as proof of existence until
122
+ the server fix ships.
123
+
115
124
  Args:
116
125
  collection: Collection name
117
126
  vector_id: Vector ID
@@ -125,6 +134,44 @@ class VectorsClient(_ApiBase):
125
134
  ServerError: If service returns error
126
135
  """
127
136
  return await self._transport.get(f"/collections/{collection}/vectors/{vector_id}")
137
+
138
+ async def insert_text_batch(
139
+ self,
140
+ collection: str,
141
+ texts: List[Dict[str, Any]],
142
+ ) -> Dict[str, Any]:
143
+ """Insert a batch of **texts** into a collection. The server
144
+ embeds each entry with the collection's configured provider
145
+ (BM25 default, FastEmbed ONNX when selected in ``config.yml``).
146
+
147
+ Wire contract: ``POST /insert_texts`` with
148
+ ``{"collection": ..., "texts": [...]}`` payload. The collection
149
+ is a top-level JSON field, **not** a path segment.
150
+
151
+ Per-entry ``id``: the server **reassigns** every inserted
152
+ vector a server-generated UUID. The original client id
153
+ round-trips on the response as ``client_id``. Callers that
154
+ need idempotency by client id should key off the returned
155
+ ``results[].client_id``, not the server-assigned UUID.
156
+
157
+ Distinct from :meth:`insert_texts` on this class, which takes
158
+ :class:`Vector` objects with pre-computed embeddings and hits
159
+ the raw-vector write path.
160
+
161
+ Args:
162
+ collection: Collection name.
163
+ texts: Entries of shape
164
+ ``{"id": "...", "text": "...", "metadata": {...}}``.
165
+
166
+ Returns:
167
+ Raw ``/insert_texts`` response dict with fields
168
+ ``collection``, ``count``, ``inserted``, ``failed``,
169
+ ``results``.
170
+ """
171
+ if not texts:
172
+ raise ValidationError("texts list cannot be empty")
173
+ payload = {"collection": collection, "texts": texts}
174
+ return await self._transport.post("/insert_texts", data=payload)
128
175
  async def delete_vectors(self, collection: str, vector_ids: List[str]) -> bool:
129
176
  """
130
177
  Delete vectors from a collection.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vectorizer_sdk
3
- Version: 3.0.0
3
+ Version: 3.2.0
4
4
  Summary: Python SDK for Vectorizer - Semantic search, vector operations, and the VectorizerRPC binary transport (default in v3.x)
5
5
  Author-email: HiveLLM Team <team@hivellm.org>
6
6
  License: Apache-2.0
@@ -58,9 +58,38 @@ Dynamic: license-file
58
58
  A comprehensive Python SDK for the Vectorizer semantic search service.
59
59
 
60
60
  **Package**: `vectorizer_sdk` (PEP 625 compliant)
61
- **Version**: 3.0.0
61
+ **Version**: 3.2.0
62
62
  **PyPI**: https://pypi.org/project/vectorizer-sdk/
63
63
 
64
+ ## v3.2 — backpressure-aware client (HTTP 429 + `Retry-After`)
65
+
66
+ The REST `VectorizerClient` honors server-side bulk-upsert
67
+ backpressure shipped in Vectorizer 3.2.0
68
+ ([#263](https://github.com/hivellm/vectorizer/issues/263)). On HTTP
69
+ `429 Too Many Requests` the client parses `Retry-After` (seconds
70
+ form, 1 s default, 30 s cap), sleeps, and retries up to 3 times
71
+ before raising a typed `RateLimitError`. Pre-3.2.0 clients bounced
72
+ 429s into a generic 5xx and lost the retry budget. Identical
73
+ semantics ship in every first-party SDK (Rust, Python, TypeScript,
74
+ Go, C#) — see `tests/test_retry_after_parse.py`.
75
+
76
+ ## v3.1 — `/insert_vectors` + stable client-id upserts
77
+
78
+ - `insert_vectors(collection, vectors, public_key=None)` — bulk-
79
+ insert pre-computed embeddings with caller-supplied vector ids.
80
+ Skips the embedding pipeline entirely.
81
+ - `insert` / `insert_texts`: the request `id` is now used verbatim
82
+ as the stored `Vector.id` (non-chunked) or as `<id>#<chunk_index>`
83
+ (chunked). Re-running the same payload upserts in place instead
84
+ of duplicating.
85
+ - Chunked vectors expose a flat payload layout (`{content,
86
+ file_path, chunk_index, parent_id, ...user_metadata}`). Legacy
87
+ nested payloads from ≤ 3.0.x stay readable during the deprecation
88
+ window.
89
+
90
+ Client-id contract: non-empty, length ≤ 256, no leading/trailing
91
+ whitespace, must not contain `#`.
92
+
64
93
  ## v3.0 — VectorizerRPC is the default transport
65
94
 
66
95
  Starting with v3.0, the recommended transport is **VectorizerRPC**: a
@@ -79,6 +108,8 @@ import vectorizer_sdk
79
108
 
80
109
  async def main():
81
110
  client = await vectorizer_sdk.connect_async("vectorizer://127.0.0.1:15503")
111
+ # `hello` and `search_basic` are RPC-only (not available on the legacy
112
+ # REST `VectorizerClient`).
82
113
  await client.hello(vectorizer_sdk.HelloPayload(client_name="my-app"))
83
114
  print(await client.list_collections())
84
115
  hits = await client.search_basic("docs", "vector database", limit=5)
@@ -140,7 +171,7 @@ for a runnable end-to-end example.
140
171
  pip install vectorizer-sdk
141
172
 
142
173
  # Or specific version
143
- pip install vectorizer-sdk==3.0.0
174
+ pip install vectorizer-sdk==3.2.0
144
175
  ```
145
176
 
146
177
  ## Package Layout (v3.x)
@@ -477,6 +508,11 @@ related = await client.get_related_files(
477
508
 
478
509
  ### Summarization Operations
479
510
 
511
+ > WARNING: The `/summarize/*` REST endpoints are documented but not yet wired
512
+ > server-side (see `DOC_GAP_ANALYSIS`). The SDK methods below
513
+ > (`summarize_text`, `summarize_context`) will fail until server wiring is
514
+ > complete.
515
+
480
516
  #### Summarize Text
481
517
  Summarize text using various methods:
482
518
 
@@ -509,6 +545,11 @@ summary = await client.summarize_context(
509
545
 
510
546
  ### Workspace Management
511
547
 
548
+ > WARNING: `add_workspace`, `list_workspaces`, and `remove_workspace` are
549
+ > exposed via the REST transport through dynamic `__getattr__` delegation,
550
+ > but they are not first-class SDK methods yet. They work at runtime but
551
+ > won't autocomplete in IDEs. A future release will add explicit methods.
552
+
512
553
  #### Add Workspace
513
554
  Add a new workspace:
514
555
 
@@ -537,6 +578,11 @@ await client.remove_workspace(
537
578
 
538
579
  ### Backup Operations
539
580
 
581
+ > WARNING: `create_backup`, `list_backups`, and `restore_backup` are
582
+ > exposed via the REST transport through dynamic `__getattr__` delegation,
583
+ > but they are not first-class SDK methods yet. They work at runtime but
584
+ > won't autocomplete in IDEs. A future release will add explicit methods.
585
+
540
586
  #### Create Backup
541
587
  Create a backup of collections:
542
588
 
@@ -664,8 +710,11 @@ await client.create_collection("documents", dimension=768)
664
710
  await client.insert_texts("documents", [
665
711
  {"id": "doc1", "text": "Sample document", "metadata": {"source": "api"}}
666
712
  ])
667
- await client.update_vector("documents", "doc1", metadata={"updated": True})
668
- await client.delete_vector("documents", "doc1")
713
+ # Update-via-reinsert: re-call `insert_texts` with the same id to replace the record.
714
+ await client.insert_texts("documents", [
715
+ {"id": "doc1", "text": "Sample document (updated)", "metadata": {"updated": True}}
716
+ ])
717
+ await client.delete_vectors("documents", ["doc1"])
669
718
 
670
719
  # Reads automatically go to replicas (load balanced)
671
720
  results = await client.search_vectors("documents", query="sample", limit=10)
@@ -702,7 +751,7 @@ The SDK automatically classifies operations:
702
751
 
703
752
  | Operation Type | Routed To | Methods |
704
753
  |---------------|-----------|---------|
705
- | **Writes** | Always Master | `insert_texts`, `insert_vectors`, `update_vector`, `delete_vector`, `create_collection`, `delete_collection` |
754
+ | **Writes** | Always Master | `insert_texts`, `insert_vectors`, `delete_vectors`, `create_collection`, `delete_collection` |
706
755
  | **Reads** | Based on `read_preference` | `search_vectors`, `get_vector`, `list_collections`, `intelligent_search`, `semantic_search`, `hybrid_search` |
707
756
 
708
757
  #### Standalone Mode (Single Node)
@@ -20,6 +20,7 @@ tests/test_intelligent_search.py
20
20
  tests/test_mock_transport.py
21
21
  tests/test_models.py
22
22
  tests/test_qdrant_advanced.py
23
+ tests/test_retry_after_parse.py
23
24
  tests/test_routing.py
24
25
  tests/test_sdk_comprehensive.py
25
26
  tests/test_simple.py
@@ -1,145 +0,0 @@
1
- """
2
- HTTP client utility for making API requests using aiohttp.
3
- """
4
-
5
- from typing import Optional, Dict, Any
6
- import aiohttp
7
- import asyncio
8
- import logging
9
-
10
- try:
11
- from ..exceptions import (
12
- NetworkError,
13
- ServerError,
14
- AuthenticationError,
15
- )
16
- except ImportError:
17
- from exceptions import (
18
- NetworkError,
19
- ServerError,
20
- AuthenticationError,
21
- )
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
-
26
- class HTTPClient:
27
- """HTTP transport client."""
28
-
29
- def __init__(
30
- self,
31
- base_url: str = "http://localhost:15002",
32
- api_key: Optional[str] = None,
33
- timeout: int = 30,
34
- max_retries: int = 3
35
- ):
36
- """
37
- Initialize HTTP client.
38
-
39
- Args:
40
- base_url: Base URL for HTTP API
41
- api_key: API key for authentication
42
- timeout: Request timeout in seconds
43
- max_retries: Maximum number of retry attempts
44
- """
45
- self.base_url = base_url.rstrip('/')
46
- self.api_key = api_key
47
- self.timeout = timeout
48
- self.max_retries = max_retries
49
- self._session: Optional[aiohttp.ClientSession] = None
50
-
51
- async def _ensure_session(self):
52
- """Ensure aiohttp session is created."""
53
- if self._session is None or self._session.closed:
54
- headers = {"Content-Type": "application/json"}
55
- if self.api_key:
56
- headers["Authorization"] = f"Bearer {self.api_key}"
57
-
58
- timeout_config = aiohttp.ClientTimeout(total=self.timeout)
59
- self._session = aiohttp.ClientSession(
60
- headers=headers,
61
- timeout=timeout_config
62
- )
63
-
64
- async def close(self):
65
- """Close the HTTP session."""
66
- if self._session and not self._session.closed:
67
- await self._session.close()
68
- self._session = None
69
-
70
- async def request(
71
- self,
72
- method: str,
73
- path: str,
74
- data: Optional[Dict[str, Any]] = None
75
- ) -> Any:
76
- """
77
- Make an HTTP request.
78
-
79
- Args:
80
- method: HTTP method
81
- path: API endpoint path
82
- data: Request data
83
-
84
- Returns:
85
- Response data
86
- """
87
- await self._ensure_session()
88
-
89
- url = f"{self.base_url}{path}"
90
-
91
- try:
92
- async with self._session.request(
93
- method,
94
- url,
95
- json=data if data else None
96
- ) as response:
97
- if response.status >= 400:
98
- error_text = await response.text()
99
- raise self._handle_error(response.status, error_text)
100
-
101
- content_type = response.headers.get('Content-Type', '')
102
- if 'application/json' in content_type:
103
- return await response.json()
104
- return await response.text()
105
-
106
- except (ServerError, AuthenticationError):
107
- raise
108
- except aiohttp.ClientError as e:
109
- raise NetworkError(f"HTTP request failed: {e}")
110
- except asyncio.TimeoutError:
111
- raise NetworkError("Request timeout")
112
- except Exception as e:
113
- raise NetworkError(f"Unknown error: {e}")
114
-
115
- async def get(self, path: str) -> Any:
116
- """Make a GET request."""
117
- return await self.request("GET", path)
118
-
119
- async def post(self, path: str, data: Optional[Dict[str, Any]] = None) -> Any:
120
- """Make a POST request."""
121
- return await self.request("POST", path, data)
122
-
123
- async def put(self, path: str, data: Optional[Dict[str, Any]] = None) -> Any:
124
- """Make a PUT request."""
125
- return await self.request("PUT", path, data)
126
-
127
- async def delete(self, path: str) -> Any:
128
- """Make a DELETE request."""
129
- return await self.request("DELETE", path)
130
-
131
- def _handle_error(self, status: int, error_text: str) -> Exception:
132
- """Handle HTTP errors and convert to appropriate exceptions."""
133
- message = f"HTTP {status}: {error_text}"
134
-
135
- if status == 401:
136
- return AuthenticationError(message)
137
- elif status == 403:
138
- return AuthenticationError("Access forbidden")
139
- elif status == 404:
140
- return ServerError("Resource not found")
141
- elif status in (429, 500, 502, 503, 504):
142
- return ServerError(message)
143
- else:
144
- return ServerError(message)
145
-
File without changes
File without changes