Skip to content

Commit 2cef5a1

Browse files
committed
ORM model now provides built-in fields by default
1 parent 4403e67 commit 2cef5a1

6 files changed

Lines changed: 54 additions & 61 deletions

File tree

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ active_count = await User.count(constraints=[
5454
Models provide autocomplete and catch errors before runtime:
5555

5656
```python
57-
class User(BubbleBaseModel, typename="user"):
57+
class User(BubbleModel, typename="user"):
5858
name: str
5959
email: str
6060
age: int
@@ -76,7 +76,7 @@ user = User(_id="123x456", name="Ada", email="ada@example.com", age="twenty-five
7676
# ValidationError: Input should be a valid integer
7777

7878
# Invalid Bubble UID caught at the model level
79-
class Order(BubbleBaseModel, typename="order"):
79+
class Order(BubbleModel, typename="order"):
8080
customer: BubbleUID
8181

8282
order = Order(_id="123x456", customer="not-a-valid-uid")
@@ -158,14 +158,14 @@ set_config_provider(get_config)
158158
Define typed models with validation:
159159

160160
```python
161-
from bubble_data_api_client import BubbleBaseModel, BubbleUID, OptionalBubbleUID
161+
from bubble_data_api_client import BubbleModel, BubbleUID, OptionalBubbleUID
162162

163-
class User(BubbleBaseModel, typename="user"):
163+
class User(BubbleModel, typename="user"):
164164
name: str
165165
email: str
166166
company: OptionalBubbleUID = None # linked Bubble record
167167

168-
class Company(BubbleBaseModel, typename="company"):
168+
class Company(BubbleModel, typename="company"):
169169
name: str
170170
industry: str
171171
```
@@ -257,9 +257,9 @@ Available constraint types: `EQUALS`, `NOT_EQUAL`, `IS_EMPTY` (any field), `IS_N
257257
Validate Bubble record IDs at the type level:
258258

259259
```python
260-
from bubble_data_api_client import BubbleBaseModel, BubbleUID, OptionalBubbleUID, OptionalBubbleUIDs
260+
from bubble_data_api_client import BubbleModel, BubbleUID, OptionalBubbleUID, OptionalBubbleUIDs
261261

262-
class Order(BubbleBaseModel, typename="order"):
262+
class Order(BubbleModel, typename="order"):
263263
customer: BubbleUID # required, validated
264264
referrer: OptionalBubbleUID = None # optional, coerces invalid to None
265265
items: OptionalBubbleUIDs = None # list of UIDs, filters invalid
@@ -316,9 +316,9 @@ This library is async-only, but you can use it in sync code:
316316

317317
```python
318318
import asyncio
319-
from bubble_data_api_client import BubbleBaseModel, constraint, ConstraintType
319+
from bubble_data_api_client import BubbleModel, constraint, ConstraintType
320320

321-
class User(BubbleBaseModel, typename="user"):
321+
class User(BubbleModel, typename="user"):
322322
name: str
323323
email: str
324324
early_access_enabled: bool = False

src/bubble_data_api_client/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from bubble_data_api_client.client.orm import BubbleBaseModel
1+
from bubble_data_api_client.client.orm import BubbleModel
22
from bubble_data_api_client.client.raw_client import RawClient
33
from bubble_data_api_client.config import (
44
BubbleConfig,
@@ -24,7 +24,7 @@
2424
"configure",
2525
"set_config_provider",
2626
# client classes
27-
"BubbleBaseModel",
27+
"BubbleModel",
2828
"RawClient",
2929
# query building
3030
"Constraint",

src/bubble_data_api_client/client/orm.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import http
22
import typing
3+
from datetime import datetime
34

45
import httpx
56
from pydantic import BaseModel as PydanticBaseModel
@@ -15,10 +16,31 @@ def _get_client() -> RawClient:
1516
return RawClient()
1617

1718

18-
class BubbleBaseModel(PydanticBaseModel):
19+
class BubbleModel(PydanticBaseModel):
20+
"""Base class for Bubble data types with built-in fields and ORM operations."""
21+
1922
_typename: typing.ClassVar[str]
2023

21-
uid: str = Field(..., alias=BubbleField.ID)
24+
uid: str = Field(
25+
...,
26+
alias=BubbleField.ID,
27+
description="Unique ID in format '{timestamp}x{random}' that identifies this record.",
28+
)
29+
created_date: datetime | None = Field(
30+
default=None,
31+
alias=BubbleField.CREATED_DATE,
32+
description="Creation date of this record. Never changes.",
33+
)
34+
modified_date: datetime | None = Field(
35+
default=None,
36+
alias=BubbleField.MODIFIED_DATE,
37+
description="Automatically updated when any changes are made to this record.",
38+
)
39+
slug: str | None = Field(
40+
default=None,
41+
alias=BubbleField.SLUG,
42+
description="User-friendly and SEO-optimized URL for this record.",
43+
)
2244

2345
def __init_subclass__(cls, *, typename: str, **kwargs: typing.Any) -> None:
2446
super().__init_subclass__(**kwargs)
@@ -72,7 +94,11 @@ async def get_many(cls, uids: list[str]) -> dict[str, typing.Self]:
7294

7395
async def save(self) -> None:
7496
async with _get_client() as client:
75-
data = self.model_dump(exclude={"uid"}, by_alias=True)
97+
# exclude uid and server-managed fields
98+
data = self.model_dump(
99+
exclude={"uid", "created_date", "modified_date", "slug"},
100+
by_alias=True,
101+
)
76102
response = await client.update(self._typename, self.uid, data)
77103
response.raise_for_status()
78104

src/bubble_data_api_client/models.py

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/tests/integration/test_orm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import pytest
22

3-
from bubble_data_api_client.client.orm import BubbleBaseModel
3+
from bubble_data_api_client.client.orm import BubbleModel
44
from bubble_data_api_client.constraints import ConstraintType, constraint
55
from bubble_data_api_client.types import BubbleField
66

77

8-
class IntegrationTestModel(BubbleBaseModel, typename="IntegrationTest"):
8+
class IntegrationTestModel(BubbleModel, typename="IntegrationTest"):
99
text: str
1010

1111

src/tests/unit/client/test_orm.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
import respx
66
from pydantic import Field
77

8-
from bubble_data_api_client.client.orm import BubbleBaseModel
8+
from bubble_data_api_client.client.orm import BubbleModel
99
from bubble_data_api_client.exceptions import UnknownFieldError
1010

1111

1212
def test_model_instantiation():
1313
"""Tests that the Pydantic model can be instantiated."""
1414

15-
class User(BubbleBaseModel, typename="user"):
15+
class User(BubbleModel, typename="user"):
1616
name: str
1717

1818
# instantiate the model, no client is needed
@@ -26,7 +26,7 @@ class User(BubbleBaseModel, typename="user"):
2626
async def test_save_uses_field_aliases(configured_client: None) -> None:
2727
"""Verify save() sends Bubble aliases, not Python field names."""
2828

29-
class Order(BubbleBaseModel, typename="order"):
29+
class Order(BubbleModel, typename="order"):
3030
company: str = Field(alias="Buying company")
3131

3232
order = Order(**{"Buying company": "Acme Corp", "_id": "abc123"})
@@ -44,7 +44,7 @@ class Order(BubbleBaseModel, typename="order"):
4444
async def test_update_single_field(configured_client: None) -> None:
4545
"""Verify update() sends only the specified field."""
4646

47-
class User(BubbleBaseModel, typename="user"):
47+
class User(BubbleModel, typename="user"):
4848
name: str
4949
email: str
5050

@@ -61,7 +61,7 @@ class User(BubbleBaseModel, typename="user"):
6161
async def test_update_translates_field_aliases(configured_client: None) -> None:
6262
"""Verify update() translates Python field names to Bubble aliases."""
6363

64-
class Order(BubbleBaseModel, typename="order"):
64+
class Order(BubbleModel, typename="order"):
6565
company: str = Field(alias="Buying company")
6666
status: str
6767

@@ -77,7 +77,7 @@ class Order(BubbleBaseModel, typename="order"):
7777
async def test_update_raises_for_unknown_field() -> None:
7878
"""Verify update() raises UnknownFieldError for fields not in the model."""
7979

80-
class User(BubbleBaseModel, typename="user"):
80+
class User(BubbleModel, typename="user"):
8181
name: str
8282

8383
with pytest.raises(UnknownFieldError, match="unknown field: nonexistent"):
@@ -88,7 +88,7 @@ class User(BubbleBaseModel, typename="user"):
8888
async def test_create_translates_field_aliases(configured_client: None) -> None:
8989
"""Verify create() translates Python field names to Bubble aliases."""
9090

91-
class Order(BubbleBaseModel, typename="order"):
91+
class Order(BubbleModel, typename="order"):
9292
company: str = Field(alias="Buying company")
9393
status: str
9494

@@ -109,7 +109,7 @@ class Order(BubbleBaseModel, typename="order"):
109109
async def test_create_raises_for_unknown_field() -> None:
110110
"""Verify create() raises UnknownFieldError for fields not in the model."""
111111

112-
class User(BubbleBaseModel, typename="user"):
112+
class User(BubbleModel, typename="user"):
113113
name: str
114114

115115
with pytest.raises(UnknownFieldError, match="unknown field: nonexistent"):
@@ -121,7 +121,7 @@ async def test_create_or_update_translates_match_aliases(configured_client: None
121121
"""Verify create_or_update() translates match field names to Bubble aliases."""
122122
from bubble_data_api_client.types import OnMultiple
123123

124-
class Order(BubbleBaseModel, typename="order"):
124+
class Order(BubbleModel, typename="order"):
125125
external_id: str = Field(alias="External ID")
126126
company: str = Field(alias="Buying company")
127127

@@ -156,7 +156,7 @@ async def test_create_or_update_translates_data_aliases(configured_client: None)
156156
"""Verify create_or_update() translates data field names to Bubble aliases."""
157157
from bubble_data_api_client.types import OnMultiple
158158

159-
class Order(BubbleBaseModel, typename="order"):
159+
class Order(BubbleModel, typename="order"):
160160
external_id: str = Field(alias="External ID")
161161
company: str = Field(alias="Buying company")
162162

@@ -185,7 +185,7 @@ async def test_create_or_update_raises_for_unknown_match_field() -> None:
185185
"""Verify create_or_update() raises UnknownFieldError for unknown match fields."""
186186
from bubble_data_api_client.types import OnMultiple
187187

188-
class User(BubbleBaseModel, typename="user"):
188+
class User(BubbleModel, typename="user"):
189189
name: str
190190

191191
with pytest.raises(UnknownFieldError, match="unknown field: nonexistent"):
@@ -200,7 +200,7 @@ async def test_create_or_update_raises_for_unknown_data_field() -> None:
200200
"""Verify create_or_update() raises UnknownFieldError for unknown data fields."""
201201
from bubble_data_api_client.types import OnMultiple
202202

203-
class User(BubbleBaseModel, typename="user"):
203+
class User(BubbleModel, typename="user"):
204204
name: str
205205

206206
with pytest.raises(UnknownFieldError, match="unknown field: nonexistent"):

0 commit comments

Comments
 (0)