Examples¶
Minimal create/load/update¶
Build common views from a single model and use them as function signatures.
from pydantic import BaseModel
from pydantic_views import BuilderCreate, BuilderCreateResult, BuilderLoad, BuilderUpdate
class User(BaseModel):
id: int
email: str
password: str
UserCreate = BuilderCreate().build_view(User)
UserCreateResult = BuilderCreateResult().build_view(User)
UserLoad = BuilderLoad().build_view(User)
UserUpdate = BuilderUpdate().build_view(User)
def create_user(input: UserCreate) -> UserCreateResult: ...
def get_user(user_id: int) -> UserLoad: ...
def update_user(user_id: int, input: UserUpdate) -> UserLoad: ...
Nested models and selective fields¶
Views cascade into nested models and respect access modes you annotate.
from typing import Annotated
from pydantic import BaseModel, computed_field
from pydantic_views import AccessMode, ReadOnly, ReadOnlyOnCreation, BuilderLoad
class Address(BaseModel):
street: str
city: str
zip_code: str
class Profile(BaseModel):
username: str
email: ReadOnly[str]
# Hide on create/update, show after creation
api_token: ReadOnlyOnCreation[str]
# Expose only when reading (load views)
score: Annotated[int, AccessMode.READ_ONLY]
@computed_field
def location(self) -> str:
return f"{self.username} @ {self.email}"
address: Address
ProfileLoad = BuilderLoad().build_view(Profile)
profile = Profile(
username="alice",
email="alice@example.com",
api_token="secret",
score=42,
address=Address(street="Main", city="Springfield", zip_code="00000"),
)
loaded = ProfileLoad.view_build_from(profile)
assert loaded.address.city == "Springfield"
# api_token and score are present, write-only fields would have been stripped
Applying partial updates¶
Update views accept only the fields you want to change and merge them into an instance.
from pydantic import BaseModel
from pydantic_views import BuilderUpdate
class Settings(BaseModel):
theme: str
locale: str
marketing_opt_in: bool
SettingsUpdate = BuilderUpdate().build_view(Settings)
current = Settings(theme="light", locale="en", marketing_opt_in=False)
patch = SettingsUpdate(theme="dark")
updated = patch.view_apply_to(current)
assert updated.theme == "dark"
assert updated.locale == "en" # unchanged
Create + result pair with computed fields¶
Use BuilderCreate to accept input and BuilderCreateResult to return read-only and computed fields.
from pydantic import BaseModel, computed_field
from pydantic_views import BuilderCreate, BuilderCreateResult, ReadOnly
class Invoice(BaseModel):
id: ReadOnly[int]
description: str
units: int
unit_price: float
@computed_field
def total(self) -> float:
return self.units * self.unit_price
InvoiceCreate = BuilderCreate().build_view(Invoice)
InvoiceCreateResult = BuilderCreateResult().build_view(Invoice)
new_invoice = InvoiceCreate(description="Hosting", units=2, unit_price=25.0)
stored = Invoice(id=1, **new_invoice.model_dump())
result = InvoiceCreateResult.view_build_from(stored)
assert result.total == 50.0
Custom builder with nullable fields¶
Craft a bespoke builder to allow nullable fields and expose only certain access modes.
from pydantic import BaseModel
from pydantic_views import Builder, AccessMode
SoftDeleteBuilder = Builder(
view_name="SoftDelete",
access_modes=(AccessMode.READ_AND_WRITE,),
all_nullable=True,
)
class Document(BaseModel):
title: str
body: str
deleted_at: float | None
DocumentSoftDelete = SoftDeleteBuilder.build_view(Document)
doc = Document(title="Plan", body="...")
soft_delete = DocumentSoftDelete(deleted_at=1720000000.0)
deleted_doc = soft_delete.view_apply_to(doc)
assert deleted_doc.deleted_at is not None