vectoramp 0.1.0__tar.gz → 0.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 (25) hide show
  1. {vectoramp-0.1.0 → vectoramp-0.2.0}/CHANGELOG.md +8 -0
  2. {vectoramp-0.1.0 → vectoramp-0.2.0}/PKG-INFO +31 -8
  3. {vectoramp-0.1.0 → vectoramp-0.2.0}/README.md +30 -7
  4. {vectoramp-0.1.0 → vectoramp-0.2.0}/pyproject.toml +1 -1
  5. {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/__init__.py +2 -1
  6. {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/client.py +9 -1
  7. {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/resources.py +147 -2
  8. {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/transport.py +1 -1
  9. {vectoramp-0.1.0 → vectoramp-0.2.0}/tests/test_client.py +90 -0
  10. {vectoramp-0.1.0 → vectoramp-0.2.0}/.github/images/logo-full-dark.svg +0 -0
  11. {vectoramp-0.1.0 → vectoramp-0.2.0}/.github/images/logo-full-light.svg +0 -0
  12. {vectoramp-0.1.0 → vectoramp-0.2.0}/.github/workflows/ci.yml +0 -0
  13. {vectoramp-0.1.0 → vectoramp-0.2.0}/.github/workflows/publish.yml +0 -0
  14. {vectoramp-0.1.0 → vectoramp-0.2.0}/.gitignore +0 -0
  15. {vectoramp-0.1.0 → vectoramp-0.2.0}/CONTRIBUTING.md +0 -0
  16. {vectoramp-0.1.0 → vectoramp-0.2.0}/LICENSE +0 -0
  17. {vectoramp-0.1.0 → vectoramp-0.2.0}/NOTICE +0 -0
  18. {vectoramp-0.1.0 → vectoramp-0.2.0}/SECURITY.md +0 -0
  19. {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/connections.py +0 -0
  20. {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/embeddings.py +0 -0
  21. {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/exceptions.py +0 -0
  22. {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/py.typed +0 -0
  23. {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/sources.py +0 -0
  24. {vectoramp-0.1.0 → vectoramp-0.2.0}/src/vectoramp/types.py +0 -0
  25. {vectoramp-0.1.0 → vectoramp-0.2.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.2.0] - 2026-07-14
8
+
9
+ ### Added
10
+
11
+ - Add dataset/vector deletion helpers for deleting vectors by id.
12
+ - Add organization secret helpers and OpenAI-backed dataset creation convenience flow.
13
+ - Document OpenAI secret setup and vector deletion examples.
14
+
7
15
  ## [0.1.0] - 2026-07-02
8
16
 
9
17
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vectoramp
3
- Version: 0.1.0
3
+ Version: 0.2.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
@@ -31,9 +31,9 @@ Description-Content-Type: text/markdown
31
31
  <div align="center">
32
32
  <a href="https://vectoramp.com/">
33
33
  <picture>
34
- <source media="(prefers-color-scheme: light)" srcset=".github/images/logo-full-light.svg">
35
- <source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-full-dark.svg">
36
- <img alt="VectorAmp Logo" src=".github/images/logo-full-dark.svg" width="50%">
34
+ <source media="(prefers-color-scheme: light)" srcset="https://vectoramp.com/logo-full-light.svg">
35
+ <source media="(prefers-color-scheme: dark)" srcset="https://vectoramp.com/logo-full-dark.svg">
36
+ <img alt="VectorAmp Logo" src="https://vectoramp.com/logo-full-dark.svg" width="50%">
37
37
  </picture>
38
38
  </a>
39
39
  </div>
@@ -87,7 +87,7 @@ client = VectorAmp(api_key="va_...", base_url="http://localhost:8080")
87
87
 
88
88
  ## Datasets
89
89
 
90
- Dataset creation always requests the SABLE index. The SDK intentionally does **not** expose an `index_type` option. Built-in helpers infer dimensions for VectorAmp 4B and OpenAI `text-embedding-3-small`/`text-embedding-3-large`.
90
+ Dataset creation always requests the SABLE index. The SDK intentionally does **not** expose an `index_type` option. For normal use, omit embedding config and VectorAmp uses the managed `VectorAmp-Embedding-4B` model with inferred dim `2560`. Built-in helpers also infer dimensions for optional OpenAI BYOM models.
91
91
 
92
92
  The only required argument is `name`. The default embedding is `VectorAmp-Embedding-4B` (`dim=2560`, `metric="cosine"`):
93
93
 
@@ -103,17 +103,32 @@ Enable hybrid (dense + sparse) indexing with `hybrid=True`:
103
103
  dataset = client.datasets.create("product-docs", hybrid=True)
104
104
  ```
105
105
 
106
- Bring your own embedding model with the `openai` helper (dimension is inferred):
106
+ Optional BYOM: use the `openai` helper only when you intentionally want an OpenAI embedding model (dimension is inferred):
107
107
 
108
108
  ```python
109
109
  from vectoramp import openai
110
110
 
111
111
  dataset = client.datasets.create(
112
- "product-docs",
112
+ "openai-docs",
113
113
  embedding=openai("small"), # or openai("large")
114
114
  )
115
115
  ```
116
116
 
117
+ To save or rotate the OpenAI API key as an organization secret first, then
118
+ create the dataset with `embedding.secret_ref`, use the helper:
119
+
120
+ ```python
121
+ dataset = client.datasets.create_openai(
122
+ name="product-docs",
123
+ api_key="sk-...",
124
+ secret_ref="emb:openai:api_key", # default
125
+ )
126
+
127
+ # Or upsert the secret directly.
128
+ client.secrets.put_openai_api_key("sk-...")
129
+ client.org_secrets.update("emb:openai:api_key", "sk-rotated")
130
+ ```
131
+
117
132
  For a custom/unknown model you must pass `dim` explicitly:
118
133
 
119
134
  ```python
@@ -184,6 +199,13 @@ dataset.add_texts(
184
199
  )
185
200
  ```
186
201
 
202
+ Delete vectors by id:
203
+
204
+ ```python
205
+ dataset.delete_vectors(["doc-001", 42])
206
+ client.datasets.delete_vectors(dataset_id, ["doc-001", 42])
207
+ ```
208
+
187
209
  ## Search
188
210
 
189
211
  Search by text:
@@ -412,13 +434,14 @@ listed first; optional arguments show their default.
412
434
 
413
435
  ### Datasets (`client.datasets` / `Dataset`)
414
436
 
415
- - `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)` → `Dataset`. Always SABLE. `dim` inferred for built-in models; required for custom models.
437
+ - `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.
416
438
  - `list(*, limit=50, offset=0)` → page with `Dataset` objects.
417
439
  - `get(dataset_id)` → `Dataset`.
418
440
  - `delete(dataset_id)` / `dataset.delete()`.
419
441
  - `stats(dataset_id)` / `dataset.stats()`.
420
442
  - `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.
421
443
  - `insert(dataset_id, vectors)` and `insert_vectors(dataset_id, vectors)` / `dataset.insert(vectors)`. Record `id` may be `str` or `int` (integers stay JSON numbers).
444
+ - `delete_vectors(dataset_id, ids, *, write_concern=None)` / `dataset.delete_vectors(ids)`.
422
445
  - `embed(dataset_id, *, text=None, texts=None)` / `dataset.embed(…)`.
423
446
  - `add_texts(dataset_id, texts, *, ids=None, metadatas=None)` / `dataset.add_texts(texts, …)`. Single string or list; ids auto-generated when omitted; copies the text into `metadata.text`.
424
447
  - `list_documents(dataset_id, *, limit=50, cursor=None, status=None)` / `dataset.list_documents(…)`.
@@ -1,9 +1,9 @@
1
1
  <div align="center">
2
2
  <a href="https://vectoramp.com/">
3
3
  <picture>
4
- <source media="(prefers-color-scheme: light)" srcset=".github/images/logo-full-light.svg">
5
- <source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-full-dark.svg">
6
- <img alt="VectorAmp Logo" src=".github/images/logo-full-dark.svg" width="50%">
4
+ <source media="(prefers-color-scheme: light)" srcset="https://vectoramp.com/logo-full-light.svg">
5
+ <source media="(prefers-color-scheme: dark)" srcset="https://vectoramp.com/logo-full-dark.svg">
6
+ <img alt="VectorAmp Logo" src="https://vectoramp.com/logo-full-dark.svg" width="50%">
7
7
  </picture>
8
8
  </a>
9
9
  </div>
@@ -57,7 +57,7 @@ client = VectorAmp(api_key="va_...", base_url="http://localhost:8080")
57
57
 
58
58
  ## Datasets
59
59
 
60
- Dataset creation always requests the SABLE index. The SDK intentionally does **not** expose an `index_type` option. Built-in helpers infer dimensions for VectorAmp 4B and OpenAI `text-embedding-3-small`/`text-embedding-3-large`.
60
+ Dataset creation always requests the SABLE index. The SDK intentionally does **not** expose an `index_type` option. For normal use, omit embedding config and VectorAmp uses the managed `VectorAmp-Embedding-4B` model with inferred dim `2560`. Built-in helpers also infer dimensions for optional OpenAI BYOM models.
61
61
 
62
62
  The only required argument is `name`. The default embedding is `VectorAmp-Embedding-4B` (`dim=2560`, `metric="cosine"`):
63
63
 
@@ -73,17 +73,32 @@ Enable hybrid (dense + sparse) indexing with `hybrid=True`:
73
73
  dataset = client.datasets.create("product-docs", hybrid=True)
74
74
  ```
75
75
 
76
- Bring your own embedding model with the `openai` helper (dimension is inferred):
76
+ Optional BYOM: use the `openai` helper only when you intentionally want an OpenAI embedding model (dimension is inferred):
77
77
 
78
78
  ```python
79
79
  from vectoramp import openai
80
80
 
81
81
  dataset = client.datasets.create(
82
- "product-docs",
82
+ "openai-docs",
83
83
  embedding=openai("small"), # or openai("large")
84
84
  )
85
85
  ```
86
86
 
87
+ To save or rotate the OpenAI API key as an organization secret first, then
88
+ create the dataset with `embedding.secret_ref`, use the helper:
89
+
90
+ ```python
91
+ dataset = client.datasets.create_openai(
92
+ name="product-docs",
93
+ api_key="sk-...",
94
+ secret_ref="emb:openai:api_key", # default
95
+ )
96
+
97
+ # Or upsert the secret directly.
98
+ client.secrets.put_openai_api_key("sk-...")
99
+ client.org_secrets.update("emb:openai:api_key", "sk-rotated")
100
+ ```
101
+
87
102
  For a custom/unknown model you must pass `dim` explicitly:
88
103
 
89
104
  ```python
@@ -154,6 +169,13 @@ dataset.add_texts(
154
169
  )
155
170
  ```
156
171
 
172
+ Delete vectors by id:
173
+
174
+ ```python
175
+ dataset.delete_vectors(["doc-001", 42])
176
+ client.datasets.delete_vectors(dataset_id, ["doc-001", 42])
177
+ ```
178
+
157
179
  ## Search
158
180
 
159
181
  Search by text:
@@ -382,13 +404,14 @@ listed first; optional arguments show their default.
382
404
 
383
405
  ### Datasets (`client.datasets` / `Dataset`)
384
406
 
385
- - `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)` → `Dataset`. Always SABLE. `dim` inferred for built-in models; required for custom models.
407
+ - `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.
386
408
  - `list(*, limit=50, offset=0)` → page with `Dataset` objects.
387
409
  - `get(dataset_id)` → `Dataset`.
388
410
  - `delete(dataset_id)` / `dataset.delete()`.
389
411
  - `stats(dataset_id)` / `dataset.stats()`.
390
412
  - `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.
391
413
  - `insert(dataset_id, vectors)` and `insert_vectors(dataset_id, vectors)` / `dataset.insert(vectors)`. Record `id` may be `str` or `int` (integers stay JSON numbers).
414
+ - `delete_vectors(dataset_id, ids, *, write_concern=None)` / `dataset.delete_vectors(ids)`.
392
415
  - `embed(dataset_id, *, text=None, texts=None)` / `dataset.embed(…)`.
393
416
  - `add_texts(dataset_id, texts, *, ids=None, metadatas=None)` / `dataset.add_texts(texts, …)`. Single string or list; ids auto-generated when omitted; copies the text into `metadata.text`.
394
417
  - `list_documents(dataset_id, *, limit=50, cursor=None, status=None)` / `dataset.list_documents(…)`.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vectoramp"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Python SDK for the VectorAmp API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -10,7 +10,7 @@ from .embeddings import (
10
10
  openai,
11
11
  )
12
12
  from .exceptions import APIError, AuthenticationError, VectorAmpError
13
- from .resources import Dataset
13
+ from .resources import Dataset, OrgSecretsResource
14
14
  from .sources import (
15
15
  ConfluenceSource,
16
16
  FileUploadSource,
@@ -36,6 +36,7 @@ __all__ = [
36
36
  "JiraSource",
37
37
  "OPENAI_TEXT_EMBEDDING_3_LARGE",
38
38
  "OPENAI_TEXT_EMBEDDING_3_SMALL",
39
+ "OrgSecretsResource",
39
40
  "S3Source",
40
41
  "VectorAmp",
41
42
  "VECTORAMP_EMBEDDING_4B",
@@ -9,7 +9,13 @@ from typing import Iterator, Optional, Type
9
9
  import httpx
10
10
 
11
11
  from .connections import ConnectionsResource
12
- from .resources import DatasetsResource, IngestionResource, IntelligenceResource, SchedulesResource
12
+ from .resources import (
13
+ DatasetsResource,
14
+ IngestionResource,
15
+ IntelligenceResource,
16
+ OrgSecretsResource,
17
+ SchedulesResource,
18
+ )
13
19
  from .transport import BaseTransport, RestTransport
14
20
  from .types import JSON, ConversationTurn
15
21
 
@@ -54,6 +60,8 @@ class VectorAmp:
54
60
  self.ingestion = IngestionResource(self.transport)
55
61
  self.sources = self.ingestion
56
62
  self.connections = ConnectionsResource(self.transport)
63
+ self.secrets = OrgSecretsResource(self.transport)
64
+ self.org_secrets = self.secrets
57
65
  self.schedules = SchedulesResource(self.transport)
58
66
  self.intelligence = IntelligenceResource(self.transport)
59
67
 
@@ -7,8 +7,15 @@ import uuid
7
7
  from collections.abc import ItemsView, KeysView, ValuesView
8
8
  from pathlib import Path
9
9
  from typing import Any, Iterator, Mapping, Optional, Sequence, Union
10
-
11
- from .embeddings import EMBEDDING_DIMENSIONS, VECTORAMP_EMBEDDING_4B
10
+ from urllib.parse import quote
11
+
12
+ from .embeddings import (
13
+ EMBEDDING_DIMENSIONS,
14
+ OPENAI_TEXT_EMBEDDING_3_SMALL,
15
+ VECTORAMP_EMBEDDING_4B,
16
+ OpenAIEmbeddingSize,
17
+ openai,
18
+ )
12
19
  from .sources import (
13
20
  ConfluenceSource,
14
21
  FileUploadSource,
@@ -137,6 +144,15 @@ class Dataset:
137
144
  """
138
145
  return self.service.insert_vectors(self.id, vectors)
139
146
 
147
+ def delete_vectors(
148
+ self,
149
+ ids: Sequence[VectorId],
150
+ *,
151
+ write_concern: Optional[Mapping[str, Any]] = None,
152
+ ) -> JSON:
153
+ """Delete vectors from this dataset by id."""
154
+ return self.service.delete_vectors(self.id, ids, write_concern=write_concern)
155
+
140
156
  def add_texts(
141
157
  self,
142
158
  texts: Union[str, Sequence[str]],
@@ -362,6 +378,9 @@ class DatasetsResource:
362
378
  filters: Optional[Mapping[str, Any]] = None,
363
379
  metadata_schema: Optional[Mapping[str, Any]] = None,
364
380
  tuning: Optional[Mapping[str, Any]] = None,
381
+ openai_api_key: Optional[str] = None,
382
+ openai_secret_ref: str = "emb:openai:api_key",
383
+ validate_openai_key: bool = False,
365
384
  ) -> Dataset:
366
385
  """Create a SABLE dataset.
367
386
 
@@ -384,13 +403,38 @@ class DatasetsResource:
384
403
  filters: Optional filter schema/configuration.
385
404
  metadata_schema: Optional metadata schema.
386
405
  tuning: Optional SABLE tuning parameters.
406
+ openai_api_key: Optional OpenAI API key to store as an organization
407
+ secret before creating the dataset. When provided, and no
408
+ explicit embedding is passed, the dataset uses OpenAI
409
+ ``text-embedding-3-small`` with ``secret_ref`` set to
410
+ ``openai_secret_ref``.
411
+ openai_secret_ref: Organization-secret name used for the OpenAI key.
412
+ validate_openai_key: Ask the API to validate the OpenAI key before
413
+ storing it. Defaults to ``False``.
387
414
 
388
415
  Returns:
389
416
  Created ``Dataset`` object.
390
417
  """
418
+ if openai_api_key is not None:
419
+ if self.client is None or not hasattr(self.client, "secrets"):
420
+ raise TypeError("openai_api_key requires a DatasetResource owned by VectorAmp.")
421
+ validation_model = str(
422
+ (embedding or {}).get("model") or OPENAI_TEXT_EMBEDDING_3_SMALL
423
+ )
424
+ self.client.secrets.put(
425
+ openai_secret_ref,
426
+ openai_api_key,
427
+ validate=validate_openai_key,
428
+ model=validation_model,
429
+ )
430
+ if embedding is None and embedding_provider == "vectoramp":
431
+ embedding = openai("small")
432
+
391
433
  embedding_config = {"provider": embedding_provider, "model": embedding_model}
392
434
  if embedding is not None:
393
435
  embedding_config.update(dict(embedding))
436
+ if openai_api_key is not None and embedding_config.get("provider") == "openai":
437
+ embedding_config["secret_ref"] = openai_secret_ref
394
438
  resolved_dim = dim or EMBEDDING_DIMENSIONS.get(str(embedding_config.get("model")))
395
439
  if resolved_dim is None:
396
440
  raise ValueError("dim is required for custom embedding models")
@@ -412,6 +456,38 @@ class DatasetsResource:
412
456
  body["tuning"] = dict(tuning)
413
457
  return self._to_dataset(self._transport.request("POST", "/datasets", json_body=body))
414
458
 
459
+ def create_openai(
460
+ self,
461
+ *,
462
+ name: str,
463
+ api_key: str,
464
+ size: OpenAIEmbeddingSize = "small",
465
+ secret_ref: str = "emb:openai:api_key",
466
+ validate: bool = False,
467
+ dim: Optional[int] = None,
468
+ metric: Metric = "cosine",
469
+ hybrid: bool = False,
470
+ filters: Optional[Mapping[str, Any]] = None,
471
+ metadata_schema: Optional[Mapping[str, Any]] = None,
472
+ tuning: Optional[Mapping[str, Any]] = None,
473
+ ) -> Dataset:
474
+ """Store an OpenAI org secret, then create an OpenAI-backed dataset."""
475
+ embedding_config = openai(size)
476
+ embedding_config["secret_ref"] = secret_ref
477
+ return self.create(
478
+ name=name,
479
+ dim=dim,
480
+ metric=metric,
481
+ embedding=embedding_config,
482
+ hybrid=hybrid,
483
+ filters=filters,
484
+ metadata_schema=metadata_schema,
485
+ tuning=tuning,
486
+ openai_api_key=api_key,
487
+ openai_secret_ref=secret_ref,
488
+ validate_openai_key=validate,
489
+ )
490
+
415
491
  def delete(self, dataset_id: str) -> Any:
416
492
  """Delete a dataset and return the API response.
417
493
 
@@ -578,6 +654,23 @@ class DatasetsResource:
578
654
  """Alias for ``insert_vectors``."""
579
655
  return self.insert_vectors(dataset_id, vectors)
580
656
 
657
+ def delete_vectors(
658
+ self,
659
+ dataset_id: str,
660
+ ids: Sequence[VectorId],
661
+ *,
662
+ write_concern: Optional[Mapping[str, Any]] = None,
663
+ ) -> JSON:
664
+ """Delete vectors from a dataset by id."""
665
+ if not ids:
666
+ raise ValueError("ids must contain at least one vector id.")
667
+ body: JSON = {"ids": list(ids)}
668
+ if write_concern is not None:
669
+ body["write_concern"] = dict(write_concern)
670
+ return self._transport.request(
671
+ "DELETE", f"/api/v1/datasets/{dataset_id}/vectors", json_body=body
672
+ )
673
+
581
674
  def embed(
582
675
  self,
583
676
  dataset_id: str,
@@ -657,6 +750,58 @@ class DatasetsResource:
657
750
  return Dataset(self, raw_data)
658
751
 
659
752
 
753
+ class OrgSecretsResource:
754
+ """Organization secret helpers."""
755
+
756
+ def __init__(self, transport: BaseTransport) -> None:
757
+ self._transport = transport
758
+
759
+ def put(
760
+ self,
761
+ name: str,
762
+ value: str,
763
+ *,
764
+ validate: bool = False,
765
+ model: str = OPENAI_TEXT_EMBEDDING_3_SMALL,
766
+ ) -> Any:
767
+ """Create or update an organization secret.
768
+
769
+ Stores provider credentials through the generic organization-secret
770
+ REST endpoint. Plaintext is sent once and is never returned by reads.
771
+ """
772
+ if not name:
773
+ raise ValueError("name is required.")
774
+ if not value:
775
+ raise ValueError("value is required.")
776
+ return self._transport.request(
777
+ "PUT",
778
+ f"/org-secrets/{quote(name, safe='')}",
779
+ json_body={"value": value},
780
+ )
781
+
782
+ def update(
783
+ self,
784
+ name: str,
785
+ value: str,
786
+ *,
787
+ validate: bool = False,
788
+ model: str = OPENAI_TEXT_EMBEDDING_3_SMALL,
789
+ ) -> Any:
790
+ """Alias for :meth:`put`; org secrets are upserted by name."""
791
+ return self.put(name, value, validate=validate, model=model)
792
+
793
+ def put_openai_api_key(
794
+ self,
795
+ api_key: str,
796
+ *,
797
+ secret_ref: str = "emb:openai:api_key",
798
+ validate: bool = False,
799
+ model: str = OPENAI_TEXT_EMBEDDING_3_SMALL,
800
+ ) -> Any:
801
+ """Create or update the OpenAI API key organization secret."""
802
+ return self.put(secret_ref, api_key, validate=validate, model=model)
803
+
804
+
660
805
  class IngestionResource:
661
806
  """Sources, upload sessions, and ingestion jobs."""
662
807
 
@@ -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.1.0"}
160
+ merged = {"X-API-Key": self.api_key, "User-Agent": "vectoramp-python/0.2.0"}
161
161
  if headers:
162
162
  merged.update(headers)
163
163
  return merged
@@ -133,6 +133,96 @@ def test_dataset_create_openai_helper_infers_dimension() -> None:
133
133
  }
134
134
 
135
135
 
136
+ def test_dataset_create_openai_stores_secret_then_uses_secret_ref() -> None:
137
+ calls: list[tuple[str, str, dict[str, Any]]] = []
138
+
139
+ def handler(request: httpx.Request) -> httpx.Response:
140
+ body = json.loads(request.content) if request.content else {}
141
+ calls.append((request.method, request.url.path, body))
142
+ if request.url.path == "/org-secrets/emb%3Aopenai%3Aapi_key":
143
+ return httpx.Response(204)
144
+ return json_response({"id": "ds_openai", "index_type": "sable"}, 201)
145
+
146
+ client = make_client(handler)
147
+ dataset = client.datasets.create_openai(
148
+ name="openai-docs",
149
+ api_key="sk-test",
150
+ secret_ref="emb:openai:customer-a",
151
+ validate=True,
152
+ )
153
+
154
+ assert dataset.id == "ds_openai"
155
+ assert calls == [
156
+ (
157
+ "PUT",
158
+ "/org-secrets/emb:openai:customer-a",
159
+ {"value": "sk-test"},
160
+ ),
161
+ (
162
+ "POST",
163
+ "/datasets",
164
+ {
165
+ "name": "openai-docs",
166
+ "dim": 1536,
167
+ "metric": "cosine",
168
+ "embedding": {
169
+ "provider": "openai",
170
+ "model": "text-embedding-3-small",
171
+ "secret_ref": "emb:openai:customer-a",
172
+ },
173
+ "index_type": "sable",
174
+ },
175
+ ),
176
+ ]
177
+
178
+
179
+ def test_org_secrets_put_update_openai_key() -> None:
180
+ calls: list[dict[str, Any]] = []
181
+
182
+ def handler(request: httpx.Request) -> httpx.Response:
183
+ calls.append(json.loads(request.content))
184
+ return httpx.Response(204)
185
+
186
+ client = make_client(handler)
187
+ assert client.secrets.put("emb:openai:api_key", "sk-new") is None
188
+ assert client.org_secrets.update("emb:openai:api_key", "sk-rotated") is None
189
+
190
+ assert calls[0]["value"] == "sk-new"
191
+ assert calls[1]["value"] == "sk-rotated"
192
+
193
+
194
+ def test_delete_vectors_payload_and_dataset_helper() -> None:
195
+ calls: list[tuple[str, str, dict[str, Any]]] = []
196
+
197
+ def handler(request: httpx.Request) -> httpx.Response:
198
+ body = json.loads(request.content) if request.content else {}
199
+ calls.append((request.method, request.url.path, body))
200
+ return json_response({"deleted": 2, "ids": body.get("ids", [])})
201
+
202
+ client = make_client(handler)
203
+ result = client.datasets.delete_vectors(
204
+ "ds_1", ["vec-a", 42], write_concern={"wait": True}
205
+ )
206
+ dataset = Dataset(client.datasets, {"id": "ds_1"})
207
+ dataset.delete_vectors(["vec-c"])
208
+
209
+ assert result["deleted"] == 2
210
+ assert calls == [
211
+ (
212
+ "DELETE",
213
+ "/api/v1/datasets/ds_1/vectors",
214
+ {"ids": ["vec-a", 42], "write_concern": {"wait": True}},
215
+ ),
216
+ ("DELETE", "/api/v1/datasets/ds_1/vectors", {"ids": ["vec-c"]}),
217
+ ]
218
+
219
+
220
+ def test_delete_vectors_requires_ids() -> None:
221
+ client = make_client(lambda request: json_response({}))
222
+ with pytest.raises(ValueError):
223
+ client.datasets.delete_vectors("ds_1", [])
224
+
225
+
136
226
  def test_list_get_delete_and_stats() -> None:
137
227
  calls = []
138
228
 
File without changes
File without changes
File without changes
File without changes
File without changes