Skip to content

Commit 1f356c2

Browse files
committed
fix(preview): add complete tag validation to PreviewIndexes.create and configure
## Purpose The SDK only validated tag key/value length but not character-set constraints or the 20-tag maximum. Users with invalid keys (e.g., containing '!') or non-ASCII values received confusing 400 errors from the backend instead of descriptive client-side errors. ## Solution Extracted tag validation into a shared helper `pinecone.preview._internal.validation.validate_tags` that enforces all three backend constraints: - Keys: alphanumeric, '_', '-' only; ≤80 chars - Values: printable ASCII only; ≤120 chars - Maximum of 20 tags Replaced the inline length-only loops in `PreviewIndexes.create`, `PreviewIndexes.configure`, `AsyncPreviewIndexes.create`, and `AsyncPreviewIndexes.configure` with a single `validate_tags(tags)` call. Updated docstrings to document all constraints. Added 13 unit tests covering valid/invalid key characters, non-ASCII values, control characters, boundary conditions (exactly 20/80/120), and None input.
1 parent 4ea947f commit 1f356c2

4 files changed

Lines changed: 123 additions & 36 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Preview-specific input validation helpers."""
2+
3+
from __future__ import annotations
4+
5+
import re
6+
7+
from pinecone.errors.exceptions import PineconeValueError
8+
9+
_TAG_KEY_RE = re.compile(r"^[a-zA-Z0-9_-]+$")
10+
_MAX_TAGS = 20
11+
_MAX_KEY_LEN = 80
12+
_MAX_VAL_LEN = 120
13+
14+
15+
def validate_tags(tags: dict[str, str] | None) -> None:
16+
"""Raise PineconeValueError if any tag violates backend constraints."""
17+
if tags is None:
18+
return
19+
if len(tags) > _MAX_TAGS:
20+
raise PineconeValueError(f"Tags exceeded the maximum of {_MAX_TAGS}. Got {len(tags)} tags.")
21+
for key, value in tags.items():
22+
if len(key) > _MAX_KEY_LEN:
23+
raise PineconeValueError(f"Tag key {key!r} exceeds the {_MAX_KEY_LEN}-character limit.")
24+
if not _TAG_KEY_RE.match(key):
25+
raise PineconeValueError(
26+
f"Tag key {key!r} has invalid characters. Must be alphanumeric or '_', '-'."
27+
)
28+
if len(value) > _MAX_VAL_LEN:
29+
raise PineconeValueError(
30+
f"Tag value for key {key!r} exceeds the {_MAX_VAL_LEN}-character limit."
31+
)
32+
if not value.isascii() or not value.isprintable():
33+
raise PineconeValueError(
34+
f"Tag value for key {key!r} contains invalid characters. "
35+
"Only printable ASCII characters are allowed."
36+
)

pinecone/preview/async_indexes.py

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
PreviewListIndexesAdapter,
2525
)
2626
from pinecone.preview._internal.constants import INDEXES_API_VERSION
27+
from pinecone.preview._internal.validation import validate_tags
2728
from pinecone.preview.models.backups import PreviewBackupModel, PreviewCreateBackupRequest
2829
from pinecone.preview.models.indexes import PreviewIndexModel
2930
from pinecone.preview.models.requests import PreviewConfigureIndexRequest, PreviewCreateIndexRequest
@@ -147,7 +148,9 @@ async def create(
147148
``"enabled"`` or ``"disabled"``. Defaults to ``"disabled"``
148149
if not provided.
149150
tags: Optional key-value tags for the index. Keys must be at most
150-
80 characters; values must be at most 120 characters.
151+
80 characters and contain only alphanumeric characters,
152+
underscores, or hyphens. Values must be at most 120 printable
153+
ASCII characters. A maximum of 20 tags is allowed.
151154
source_collection: Optional name of an existing collection to
152155
create the index from.
153156
source_backup_id: Optional ID of an existing backup to create
@@ -162,7 +165,9 @@ async def create(
162165
163166
Raises:
164167
:exc:`~pinecone.errors.exceptions.PineconeValueError`: If a tag key
165-
exceeds 80 characters or a tag value exceeds 120 characters.
168+
exceeds 80 characters, contains invalid characters, if a tag
169+
value exceeds 120 characters or contains non-ASCII/non-printable
170+
characters, or if more than 20 tags are provided.
166171
:exc:`~pinecone.errors.exceptions.ApiError`: If the API returns an
167172
error response.
168173
@@ -174,14 +179,7 @@ async def create(
174179
name="my-preview-index",
175180
)
176181
"""
177-
if tags is not None:
178-
for key, value in tags.items():
179-
if len(key) > 80:
180-
raise PineconeValueError(f"Tag key {key!r} exceeds the 80-character limit.")
181-
if len(value) > 120:
182-
raise PineconeValueError(
183-
f"Tag value for key {key!r} exceeds the 120-character limit."
184-
)
182+
validate_tags(tags)
185183

186184
req = PreviewCreateIndexRequest(
187185
schema=schema,
@@ -318,14 +316,7 @@ async def main():
318316
"schema, deletion_protection, tags, read_capacity, or deployment"
319317
)
320318

321-
if tags is not None:
322-
for key, value in tags.items():
323-
if len(key) > 80:
324-
raise PineconeValueError(f"Tag key {key!r} exceeds the 80-character limit.")
325-
if len(value) > 120:
326-
raise PineconeValueError(
327-
f"Tag value for key {key!r} exceeds the 120-character limit."
328-
)
319+
validate_tags(tags)
329320

330321
req = PreviewConfigureIndexRequest(
331322
schema=schema,

pinecone/preview/indexes.py

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
PreviewListIndexesAdapter,
2424
)
2525
from pinecone.preview._internal.constants import INDEXES_API_VERSION
26+
from pinecone.preview._internal.validation import validate_tags
2627
from pinecone.preview.models.backups import PreviewBackupModel, PreviewCreateBackupRequest
2728
from pinecone.preview.models.indexes import PreviewIndexModel
2829
from pinecone.preview.models.requests import PreviewConfigureIndexRequest, PreviewCreateIndexRequest
@@ -137,7 +138,9 @@ def create(
137138
``"enabled"`` or ``"disabled"``. Defaults to ``"disabled"``
138139
if not provided.
139140
tags: Optional key-value tags for the index. Keys must be at most
140-
80 characters; values must be at most 120 characters.
141+
80 characters and contain only alphanumeric characters,
142+
underscores, or hyphens. Values must be at most 120 printable
143+
ASCII characters. A maximum of 20 tags is allowed.
141144
source_collection: Optional name of an existing collection to
142145
create the index from.
143146
source_backup_id: Optional ID of an existing backup to create
@@ -152,7 +155,9 @@ def create(
152155
153156
Raises:
154157
:exc:`~pinecone.errors.exceptions.PineconeValueError`: If a tag key
155-
exceeds 80 characters or a tag value exceeds 120 characters.
158+
exceeds 80 characters, contains invalid characters, if a tag
159+
value exceeds 120 characters or contains non-ASCII/non-printable
160+
characters, or if more than 20 tags are provided.
156161
:exc:`~pinecone.errors.exceptions.ApiError`: If the API returns an
157162
error response.
158163
@@ -164,14 +169,7 @@ def create(
164169
name="my-preview-index",
165170
)
166171
"""
167-
if tags is not None:
168-
for key, value in tags.items():
169-
if len(key) > 80:
170-
raise PineconeValueError(f"Tag key {key!r} exceeds the 80-character limit.")
171-
if len(value) > 120:
172-
raise PineconeValueError(
173-
f"Tag value for key {key!r} exceeds the 120-character limit."
174-
)
172+
validate_tags(tags)
175173

176174
req = PreviewCreateIndexRequest(
177175
schema=schema,
@@ -304,14 +302,7 @@ def configure(
304302
"schema, deletion_protection, tags, read_capacity, or deployment"
305303
)
306304

307-
if tags is not None:
308-
for key, value in tags.items():
309-
if len(key) > 80:
310-
raise PineconeValueError(f"Tag key {key!r} exceeds the 80-character limit.")
311-
if len(value) > 120:
312-
raise PineconeValueError(
313-
f"Tag value for key {key!r} exceeds the 120-character limit."
314-
)
305+
validate_tags(tags)
315306

316307
req = PreviewConfigureIndexRequest(
317308
schema=schema,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Unit tests for preview tag validation helper."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from pinecone.errors.exceptions import PineconeValueError
8+
from pinecone.preview._internal.validation import validate_tags
9+
10+
11+
def test_validate_tags_none_ok() -> None:
12+
validate_tags(None)
13+
14+
15+
def test_validate_tags_valid_ok() -> None:
16+
validate_tags({"key-1": "val1", "key_2": "val2"})
17+
18+
19+
def test_validate_tags_empty_value_ok() -> None:
20+
validate_tags({"key": ""})
21+
22+
23+
def test_validate_tags_special_char_key_raises() -> None:
24+
with pytest.raises(PineconeValueError, match="invalid characters"):
25+
validate_tags({"bad-key!": "val"})
26+
27+
28+
def test_validate_tags_non_ascii_key_raises() -> None:
29+
with pytest.raises(PineconeValueError, match="invalid characters"):
30+
validate_tags({"藏": "val"})
31+
32+
33+
def test_validate_tags_non_ascii_value_raises() -> None:
34+
with pytest.raises(PineconeValueError, match="invalid characters"):
35+
validate_tags({"key": "日本語"})
36+
37+
38+
def test_validate_tags_control_char_value_raises() -> None:
39+
with pytest.raises(PineconeValueError, match="invalid characters"):
40+
validate_tags({"key": "val\x00"})
41+
42+
43+
def test_validate_tags_too_many_raises() -> None:
44+
tags = {f"key{i}": f"val{i}" for i in range(21)}
45+
with pytest.raises(PineconeValueError, match="maximum of 20"):
46+
validate_tags(tags)
47+
48+
49+
def test_validate_tags_exactly_20_ok() -> None:
50+
tags = {f"key{i}": f"val{i}" for i in range(20)}
51+
validate_tags(tags)
52+
53+
54+
def test_validate_tags_key_too_long_raises() -> None:
55+
with pytest.raises(PineconeValueError, match="80-character limit"):
56+
validate_tags({"a" * 81: "val"})
57+
58+
59+
def test_validate_tags_key_exactly_80_ok() -> None:
60+
validate_tags({"a" * 80: "val"})
61+
62+
63+
def test_validate_tags_value_too_long_raises() -> None:
64+
with pytest.raises(PineconeValueError, match="120-character limit"):
65+
validate_tags({"key": "v" * 121})
66+
67+
68+
def test_validate_tags_value_exactly_120_ok() -> None:
69+
validate_tags({"key": "v" * 120})

0 commit comments

Comments
 (0)