vectoramp 0.2.0__tar.gz → 0.3.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 (25) hide show
  1. {vectoramp-0.2.0 → vectoramp-0.3.0}/CHANGELOG.md +8 -0
  2. {vectoramp-0.2.0 → vectoramp-0.3.0}/PKG-INFO +19 -1
  3. {vectoramp-0.2.0 → vectoramp-0.3.0}/README.md +18 -0
  4. {vectoramp-0.2.0 → vectoramp-0.3.0}/pyproject.toml +1 -1
  5. {vectoramp-0.2.0 → vectoramp-0.3.0}/src/vectoramp/__init__.py +4 -0
  6. {vectoramp-0.2.0 → vectoramp-0.3.0}/src/vectoramp/resources.py +46 -4
  7. {vectoramp-0.2.0 → vectoramp-0.3.0}/src/vectoramp/transport.py +1 -1
  8. {vectoramp-0.2.0 → vectoramp-0.3.0}/src/vectoramp/types.py +12 -0
  9. {vectoramp-0.2.0 → vectoramp-0.3.0}/tests/test_client.py +45 -4
  10. {vectoramp-0.2.0 → vectoramp-0.3.0}/.github/images/logo-full-dark.svg +0 -0
  11. {vectoramp-0.2.0 → vectoramp-0.3.0}/.github/images/logo-full-light.svg +0 -0
  12. {vectoramp-0.2.0 → vectoramp-0.3.0}/.github/workflows/ci.yml +0 -0
  13. {vectoramp-0.2.0 → vectoramp-0.3.0}/.github/workflows/publish.yml +0 -0
  14. {vectoramp-0.2.0 → vectoramp-0.3.0}/.gitignore +0 -0
  15. {vectoramp-0.2.0 → vectoramp-0.3.0}/CONTRIBUTING.md +0 -0
  16. {vectoramp-0.2.0 → vectoramp-0.3.0}/LICENSE +0 -0
  17. {vectoramp-0.2.0 → vectoramp-0.3.0}/NOTICE +0 -0
  18. {vectoramp-0.2.0 → vectoramp-0.3.0}/SECURITY.md +0 -0
  19. {vectoramp-0.2.0 → vectoramp-0.3.0}/src/vectoramp/client.py +0 -0
  20. {vectoramp-0.2.0 → vectoramp-0.3.0}/src/vectoramp/connections.py +0 -0
  21. {vectoramp-0.2.0 → vectoramp-0.3.0}/src/vectoramp/embeddings.py +0 -0
  22. {vectoramp-0.2.0 → vectoramp-0.3.0}/src/vectoramp/exceptions.py +0 -0
  23. {vectoramp-0.2.0 → vectoramp-0.3.0}/src/vectoramp/py.typed +0 -0
  24. {vectoramp-0.2.0 → vectoramp-0.3.0}/src/vectoramp/sources.py +0 -0
  25. {vectoramp-0.2.0 → vectoramp-0.3.0}/tests/test_connections_and_sources.py +0 -0
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project follows semantic versioning.
6
6
 
7
+ ## [0.3.0] - 2026-07-20
8
+
9
+ ### Added
10
+
11
+ - Add typed metadata-schema fields when creating datasets.
12
+ - Add metadata-schema merge/patch and full replacement operations.
13
+ - Document create, merge, and replace schema workflows.
14
+
7
15
  ## [0.2.0] - 2026-07-14
8
16
 
9
17
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vectoramp
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for the VectorAmp API
5
5
  Project-URL: Homepage, https://vectoramp.com
6
6
  Project-URL: Documentation, https://docs.vectoramp.com
@@ -129,6 +129,22 @@ client.secrets.put_openai_api_key("sk-...")
129
129
  client.org_secrets.update("emb:openai:api_key", "sk-rotated")
130
130
  ```
131
131
 
132
+ Declare typed metadata at creation, then merge fields or replace the complete schema:
133
+
134
+ ```python
135
+ from vectoramp import MetadataSchemaField
136
+
137
+ schema: list[MetadataSchemaField] = [
138
+ {"name": "category", "type": "string"},
139
+ {"name": "price", "type": "f32"},
140
+ ]
141
+ dataset = client.datasets.create(name="products", metadata_schema=schema)
142
+ dataset.patch_metadata_schema([{"name": "inventory", "type": "u32"}])
143
+ dataset.replace_metadata_schema(schema) # [] removes all declared fields
144
+ ```
145
+
146
+ Canonical field types are `string`, `u32`, `i32`, `i64`, `f32`, and `f64`.
147
+
132
148
  For a custom/unknown model you must pass `dim` explicitly:
133
149
 
134
150
  ```python
@@ -437,6 +453,8 @@ listed first; optional arguments show their default.
437
453
  - `create(name, *, dim=None, metric="cosine", embedding=None, embedding_provider="vectoramp", embedding_model="VectorAmp-Embedding-4B", hybrid=False, filters=None, metadata_schema=None, tuning=None, openai_api_key=None, openai_secret_ref="emb:openai:api_key", validate_openai_key=False)` → `Dataset`. Always SABLE. `dim` inferred for built-in models; required for custom models.
438
454
  - `list(*, limit=50, offset=0)` → page with `Dataset` objects.
439
455
  - `get(dataset_id)` → `Dataset`.
456
+ - `patch_metadata_schema(dataset_id, schema)` / `Dataset.patch_metadata_schema(schema)` → updated `Dataset`.
457
+ - `replace_metadata_schema(dataset_id, schema)` / `Dataset.replace_metadata_schema(schema)` → updated `Dataset`.
440
458
  - `delete(dataset_id)` / `dataset.delete()`.
441
459
  - `stats(dataset_id)` / `dataset.stats()`.
442
460
  - `search(dataset_id, query=None, *, vector=None, text=None, search_text=None, top_k=10, filters=None, advanced_filters=None, embedding_provider=None, embedding_model=None, nprobe_override=None, rerank_depth_override=None, hybrid=None, sparse_query=None, alpha=None, include_embeddings=None, include_documents=None, include_metadata=None, rerank=None)` / `dataset.search(…)`. `query` accepts a string (text) or float sequence (vector); `top_k` defaults to 10.
@@ -99,6 +99,22 @@ client.secrets.put_openai_api_key("sk-...")
99
99
  client.org_secrets.update("emb:openai:api_key", "sk-rotated")
100
100
  ```
101
101
 
102
+ Declare typed metadata at creation, then merge fields or replace the complete schema:
103
+
104
+ ```python
105
+ from vectoramp import MetadataSchemaField
106
+
107
+ schema: list[MetadataSchemaField] = [
108
+ {"name": "category", "type": "string"},
109
+ {"name": "price", "type": "f32"},
110
+ ]
111
+ dataset = client.datasets.create(name="products", metadata_schema=schema)
112
+ dataset.patch_metadata_schema([{"name": "inventory", "type": "u32"}])
113
+ dataset.replace_metadata_schema(schema) # [] removes all declared fields
114
+ ```
115
+
116
+ Canonical field types are `string`, `u32`, `i32`, `i64`, `f32`, and `f64`.
117
+
102
118
  For a custom/unknown model you must pass `dim` explicitly:
103
119
 
104
120
  ```python
@@ -407,6 +423,8 @@ listed first; optional arguments show their default.
407
423
  - `create(name, *, dim=None, metric="cosine", embedding=None, embedding_provider="vectoramp", embedding_model="VectorAmp-Embedding-4B", hybrid=False, filters=None, metadata_schema=None, tuning=None, openai_api_key=None, openai_secret_ref="emb:openai:api_key", validate_openai_key=False)` → `Dataset`. Always SABLE. `dim` inferred for built-in models; required for custom models.
408
424
  - `list(*, limit=50, offset=0)` → page with `Dataset` objects.
409
425
  - `get(dataset_id)` → `Dataset`.
426
+ - `patch_metadata_schema(dataset_id, schema)` / `Dataset.patch_metadata_schema(schema)` → updated `Dataset`.
427
+ - `replace_metadata_schema(dataset_id, schema)` / `Dataset.replace_metadata_schema(schema)` → updated `Dataset`.
410
428
  - `delete(dataset_id)` / `dataset.delete()`.
411
429
  - `stats(dataset_id)` / `dataset.stats()`.
412
430
  - `search(dataset_id, query=None, *, vector=None, text=None, search_text=None, top_k=10, filters=None, advanced_filters=None, embedding_provider=None, embedding_model=None, nprobe_override=None, rerank_depth_override=None, hybrid=None, sparse_query=None, alpha=None, include_embeddings=None, include_documents=None, include_metadata=None, rerank=None)` / `dataset.search(…)`. `query` accepts a string (text) or float sequence (vector); `top_k` defaults to 10.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vectoramp"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Python SDK for the VectorAmp API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -21,6 +21,7 @@ from .sources import (
21
21
  S3Source,
22
22
  WebSource,
23
23
  )
24
+ from .types import MetadataFieldType, MetadataSchema, MetadataSchemaField
24
25
 
25
26
  __all__ = [
26
27
  "APIError",
@@ -34,6 +35,9 @@ __all__ = [
34
35
  "GenericSource",
35
36
  "GoogleDriveSource",
36
37
  "JiraSource",
38
+ "MetadataFieldType",
39
+ "MetadataSchema",
40
+ "MetadataSchemaField",
37
41
  "OPENAI_TEXT_EMBEDDING_3_LARGE",
38
42
  "OPENAI_TEXT_EMBEDDING_3_SMALL",
39
43
  "OrgSecretsResource",
@@ -6,7 +6,7 @@ import mimetypes
6
6
  import uuid
7
7
  from collections.abc import ItemsView, KeysView, ValuesView
8
8
  from pathlib import Path
9
- from typing import Any, Iterator, Mapping, Optional, Sequence, Union
9
+ from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence, Union
10
10
  from urllib.parse import quote
11
11
 
12
12
  from .embeddings import (
@@ -28,7 +28,17 @@ from .sources import (
28
28
  WebSource,
29
29
  )
30
30
  from .transport import BaseTransport, RestTransport
31
- from .types import JSON, AdvancedFilter, ConversationTurn, Filters, Metric, Vector, VectorId
31
+ from .types import (
32
+ JSON,
33
+ AdvancedFilter,
34
+ ConversationTurn,
35
+ Filters,
36
+ MetadataSchema,
37
+ MetadataSchemaInput,
38
+ Metric,
39
+ Vector,
40
+ VectorId,
41
+ )
32
42
 
33
43
  PathLike = Union[str, Path]
34
44
 
@@ -290,6 +300,14 @@ class Dataset:
290
300
  pipeline_id=pipeline_id,
291
301
  )
292
302
 
303
+ def patch_metadata_schema(self, schema: MetadataSchema) -> "Dataset":
304
+ """Add or update typed metadata fields while retaining other fields."""
305
+ return self.service.patch_metadata_schema(self.id, schema)
306
+
307
+ def replace_metadata_schema(self, schema: MetadataSchema) -> "Dataset":
308
+ """Replace the complete typed metadata schema."""
309
+ return self.service.replace_metadata_schema(self.id, schema)
310
+
293
311
  def get(self, key: str, default: Any = None) -> Any:
294
312
  """Return a value from the raw dataset payload, or ``default``."""
295
313
  return self.raw_data.get(key, default)
@@ -376,7 +394,7 @@ class DatasetsResource:
376
394
  embedding_model: str = VECTORAMP_EMBEDDING_4B,
377
395
  hybrid: bool = False,
378
396
  filters: Optional[Mapping[str, Any]] = None,
379
- metadata_schema: Optional[Mapping[str, Any]] = None,
397
+ metadata_schema: Optional[MetadataSchemaInput] = None,
380
398
  tuning: Optional[Mapping[str, Any]] = None,
381
399
  openai_api_key: Optional[str] = None,
382
400
  openai_secret_ref: str = "emb:openai:api_key",
@@ -451,7 +469,7 @@ class DatasetsResource:
451
469
  if filters is not None:
452
470
  body["filters"] = dict(filters)
453
471
  if metadata_schema is not None:
454
- body["metadata_schema"] = dict(metadata_schema)
472
+ body["schema"] = self._normalize_metadata_schema(metadata_schema)
455
473
  if tuning is not None:
456
474
  body["tuning"] = dict(tuning)
457
475
  return self._to_dataset(self._transport.request("POST", "/datasets", json_body=body))
@@ -488,6 +506,30 @@ class DatasetsResource:
488
506
  validate_openai_key=validate,
489
507
  )
490
508
 
509
+ def patch_metadata_schema(self, dataset_id: str, schema: MetadataSchema) -> Dataset:
510
+ """Add or update schema fields without removing omitted fields."""
511
+ body = {"schema": [dict(field) for field in schema], "mode": "merge"}
512
+ return self._to_dataset(
513
+ self._transport.request("PATCH", f"/datasets/{dataset_id}/schema", json_body=body)
514
+ )
515
+
516
+ def replace_metadata_schema(self, dataset_id: str, schema: MetadataSchema) -> Dataset:
517
+ """Replace a dataset's complete typed metadata schema."""
518
+ body = {"schema": [dict(field) for field in schema], "mode": "replace"}
519
+ return self._to_dataset(
520
+ self._transport.request("PATCH", f"/datasets/{dataset_id}/schema", json_body=body)
521
+ )
522
+
523
+ @staticmethod
524
+ def _normalize_metadata_schema(schema: MetadataSchemaInput) -> List[Dict[str, Any]]:
525
+ """Normalize the legacy field-name mapping and the canonical field array."""
526
+ if isinstance(schema, Mapping):
527
+ return [
528
+ {"name": name, **dict(config)}
529
+ for name, config in schema.items()
530
+ ]
531
+ return [dict(field) for field in schema]
532
+
491
533
  def delete(self, dataset_id: str) -> Any:
492
534
  """Delete a dataset and return the API response.
493
535
 
@@ -157,7 +157,7 @@ class RestTransport(BaseTransport):
157
157
  return urljoin(self.base_url, path.lstrip("/"))
158
158
 
159
159
  def _headers(self, headers: Optional[Mapping[str, str]] = None) -> Dict[str, str]:
160
- merged = {"X-API-Key": self.api_key, "User-Agent": "vectoramp-python/0.2.0"}
160
+ merged = {"X-API-Key": self.api_key, "User-Agent": "vectoramp-python/0.3.0"}
161
161
  if headers:
162
162
  merged.update(headers)
163
163
  return merged
@@ -7,6 +7,18 @@ from typing import Any, Dict, List, Literal, Mapping, Optional, Sequence, TypedD
7
7
  JSON = Dict[str, Any]
8
8
  Metadata = Mapping[str, Any]
9
9
  Metric = Literal["cosine", "dot", "euclidean"]
10
+ MetadataFieldType = Literal["string", "u32", "i32", "i64", "f32", "f64"]
11
+
12
+
13
+ class MetadataSchemaField(TypedDict):
14
+ """A typed metadata field declared on a dataset."""
15
+
16
+ name: str
17
+ type: MetadataFieldType
18
+
19
+
20
+ MetadataSchema = Sequence[MetadataSchemaField]
21
+ MetadataSchemaInput = Union[MetadataSchema, Mapping[str, Mapping[str, Any]]]
10
22
 
11
23
 
12
24
  class EmbeddingConfig(TypedDict):
@@ -52,7 +52,7 @@ def test_dataset_create_forces_sable_and_auth_header() -> None:
52
52
  name="docs",
53
53
  dim=2560,
54
54
  filters={"category": "string"},
55
- metadata_schema={"title": {"type": "string"}},
55
+ metadata_schema=[{"name": "title", "type": "string"}],
56
56
  tuning={"replicas": 1},
57
57
  )
58
58
 
@@ -64,8 +64,10 @@ def test_dataset_create_forces_sable_and_auth_header() -> None:
64
64
  "provider": "vectoramp",
65
65
  "model": "VectorAmp-Embedding-4B",
66
66
  }
67
+ # hybrid is not sent unless explicitly requested.
68
+ assert "hybrid" not in seen["body"]
67
69
  assert seen["body"]["filters"] == {"category": "string"}
68
- assert seen["body"]["metadata_schema"] == {"title": {"type": "string"}}
70
+ assert seen["body"]["schema"] == [{"name": "title", "type": "string"}]
69
71
  assert seen["body"]["tuning"] == {"replicas": 1}
70
72
 
71
73
 
@@ -88,8 +90,47 @@ def test_dataset_create_minimal_name_only_defaults() -> None:
88
90
  "provider": "vectoramp",
89
91
  "model": "VectorAmp-Embedding-4B",
90
92
  }
91
- # hybrid is not sent unless explicitly requested.
92
- assert "hybrid" not in seen["body"]
93
+
94
+
95
+ def test_dataset_create_normalizes_legacy_metadata_schema_mapping() -> None:
96
+ seen = {}
97
+
98
+ def handler(request: httpx.Request) -> httpx.Response:
99
+ seen["body"] = json.loads(request.content)
100
+ return json_response({"id": "ds_legacy"}, 201)
101
+
102
+ client = make_client(handler)
103
+ client.datasets.create(
104
+ name="legacy",
105
+ metadata_schema={"title": {"type": "string"}},
106
+ )
107
+
108
+ assert seen["body"]["schema"] == [{"name": "title", "type": "string"}]
109
+
110
+
111
+ @pytest.mark.parametrize(
112
+ ("method_name", "mode"),
113
+ [("patch_metadata_schema", "merge"), ("replace_metadata_schema", "replace")],
114
+ )
115
+ def test_update_metadata_schema(method_name: str, mode: str) -> None:
116
+ seen = {}
117
+
118
+ def handler(request: httpx.Request) -> httpx.Response:
119
+ seen["method"] = request.method
120
+ seen["url"] = str(request.url)
121
+ seen["body"] = json.loads(request.content)
122
+ return json_response({"id": "ds_1", "schema_version": 2})
123
+
124
+ client = make_client(handler)
125
+ schema = [{"name": "price", "type": "f32"}]
126
+ dataset = getattr(client.datasets, method_name)("ds_1", schema)
127
+
128
+ assert dataset.id == "ds_1"
129
+ assert seen == {
130
+ "method": "PATCH",
131
+ "url": "https://api.test/datasets/ds_1/schema",
132
+ "body": {"schema": schema, "mode": mode},
133
+ }
93
134
 
94
135
 
95
136
  def test_dataset_create_hybrid() -> None:
File without changes
File without changes
File without changes
File without changes
File without changes