Skip to content

Commit 4d2d69b

Browse files
committed
add Bubble UID validation types and utilities
1 parent 34155a4 commit 4d2d69b

7 files changed

Lines changed: 490 additions & 2 deletions

File tree

src/bubble_data_api_client/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,27 @@
55
set_config_provider,
66
)
77
from bubble_data_api_client.pool import client_scope, close_clients
8+
from bubble_data_api_client.types import (
9+
BubbleUID,
10+
OptionalBubbleUID,
11+
OptionalBubbleUIDs,
12+
)
13+
from bubble_data_api_client.validation import filter_bubble_uids, is_bubble_uid
814

915
__all__ = [
16+
# config
1017
"BubbleConfig",
1118
"ConfigProvider",
1219
"configure",
1320
"set_config_provider",
21+
# client lifecycle
1422
"client_scope",
1523
"close_clients",
24+
# types
25+
"BubbleUID",
26+
"OptionalBubbleUID",
27+
"OptionalBubbleUIDs",
28+
# validation
29+
"filter_bubble_uids",
30+
"is_bubble_uid",
1631
]

src/bubble_data_api_client/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,11 @@ class BubbleNotFoundError(BubbleHttpError):
1919

2020
class BubbleUnauthorizedError(BubbleHttpError):
2121
"""Raised when the user is not authorized to access a resource."""
22+
23+
24+
class InvalidBubbleUIDError(ValueError):
25+
"""Raised when a string is not a valid Bubble UID."""
26+
27+
def __init__(self, value: str) -> None:
28+
super().__init__(f"invalid Bubble UID format: {value}")
29+
self.value = value

src/bubble_data_api_client/models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ class BubbleThing(BaseModel):
1212
id: str = Field(
1313
...,
1414
alias="_id",
15-
description="The Unique ID is a 32-character alphanumeric string "
16-
"that identifies a specific thing in the database.",
15+
description="The Unique ID in format '{timestamp}x{random}' that identifies a specific thing in the database.",
1716
)
1817
created_date: datetime = Field(
1918
...,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Bubble platform types for use with Pydantic models."""
2+
3+
from typing import Annotated, Any
4+
5+
from pydantic import AfterValidator, BeforeValidator
6+
7+
from bubble_data_api_client.exceptions import InvalidBubbleUIDError
8+
from bubble_data_api_client.validation import is_bubble_uid
9+
10+
11+
def _validate_bubble_uid(value: str) -> str:
12+
"""Validate that a string is a valid Bubble UID."""
13+
if not is_bubble_uid(value):
14+
raise InvalidBubbleUIDError(value)
15+
return value
16+
17+
18+
BubbleUID = Annotated[str, AfterValidator(_validate_bubble_uid)]
19+
"""A string validated as a Bubble UID (format: digits + 'x' + digits)."""
20+
21+
22+
def _coerce_optional_bubble_uid(value: Any) -> str | None:
23+
"""Coerce to valid Bubble UID or None. Invalid values silently become None."""
24+
if value is None or value == "":
25+
return None
26+
if not isinstance(value, str):
27+
return None
28+
if not is_bubble_uid(value):
29+
return None
30+
return value
31+
32+
33+
OptionalBubbleUID = Annotated[str | None, BeforeValidator(_coerce_optional_bubble_uid)]
34+
"""A Bubble UID that silently coerces invalid values (including empty string) to None."""
35+
36+
37+
def _coerce_optional_bubble_uids(value: object) -> list[str] | None:
38+
"""Coerce to list of valid Bubble UIDs or None. Empty/invalid becomes None."""
39+
if not isinstance(value, list):
40+
return None
41+
result = [x for x in value if isinstance(x, str) and is_bubble_uid(x)]
42+
return result or None
43+
44+
45+
OptionalBubbleUIDs = Annotated[list[str] | None, BeforeValidator(_coerce_optional_bubble_uids)]
46+
"""A list of Bubble UIDs that silently coerces invalid/empty to None."""
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Validation utilities for Bubble platform data."""
2+
3+
import re
4+
from collections.abc import Iterable
5+
6+
_BUBBLE_UID_PATTERN: re.Pattern[str] = re.compile(r"^[0-9]+x[0-9]+$")
7+
8+
9+
def is_bubble_uid(value: str) -> bool:
10+
"""Check if a string matches the Bubble UID format (e.g., '1767090310181x452059685440531200')."""
11+
return _BUBBLE_UID_PATTERN.fullmatch(value) is not None
12+
13+
14+
def filter_bubble_uids(values: Iterable[str]) -> list[str]:
15+
"""Return only valid Bubble UIDs from an iterable, filtering out invalid ones."""
16+
return [v for v in values if is_bubble_uid(v)]

0 commit comments

Comments
 (0)