Fields and Protocols: Best Usage Patterns¶
This guide demonstrates the best practices for using pydapter's field system with protocols to create consistent, type-safe models for database operations.
Core Concepts¶
1. Field Templates vs Direct Fields¶
- Field Templates: Reusable definitions that can create fields with different names
- Protocol Families: Pre-defined field sets that match protocol requirements
- Field Families: Logical groupings for common database patterns
2. Protocol Alignment¶
Protocols define behavioral contracts. Use protocol-specific field families to ensure your models can leverage protocol functionality.
Important Note: The create_protocol_model
function provides structural
compliance by adding the required fields for protocols. It does NOT add behavioral
methods from protocol mixins. If you need methods like update_timestamp()
from
TemporalMixin
, you must explicitly inherit from the mixin classes (see examples below).
Pattern 1: Basic Entity with Protocols¶
For most database entities, combine Identifiable and Temporal protocols:
from pydapter.fields import create_protocol_model, FieldTemplate
from pydapter.protocols import IDENTIFIABLE, TEMPORAL
# Simple entity (structure only)
User = create_protocol_model(
"User",
IDENTIFIABLE,
TEMPORAL,
# Add domain-specific fields
username=FieldTemplate(base_type=str, max_length=50),
email=FieldTemplate(base_type=str),
is_active=FieldTemplate(base_type=bool, default=True)
)
# The model now has: id, created_at, updated_at, username, email, is_active
# But NO behavioral methods yet
# For full protocol compliance with methods, use create_protocol_model_class:
from pydapter.protocols import create_protocol_model_class
User = create_protocol_model_class(
"User",
IDENTIFIABLE,
TEMPORAL,
username=FieldTemplate(base_type=str, max_length=50),
email=FieldTemplate(base_type=str),
is_active=FieldTemplate(base_type=bool, default=True)
)
# Now has both fields AND methods like update_timestamp()
Pattern 2: Versioned Entities with Audit Trail¶
For entities requiring version control and audit tracking:
from pydapter.fields import DomainModelBuilder, FieldTemplate
Order = (
DomainModelBuilder("Order")
.with_entity_fields(timezone_aware=True) # id, created_at, updated_at
.with_audit_fields() # created_by, updated_by, version
.add_field("order_number", FieldTemplate(
base_type=str,
description="Unique order identifier"
))
.add_field("total_amount", FieldTemplate(
base_type=float,
ge=0,
description="Order total"
))
.add_field("status", FieldTemplate(
base_type=str,
default="pending",
description="Order status"
))
.build()
)
Pattern 3: Soft-Deletable Entities¶
For entities that should never be hard-deleted:
from pydapter.fields import DomainModelBuilder, FieldTemplate
Product = (
DomainModelBuilder("Product")
.with_entity_fields()
.with_soft_delete() # Adds deleted_at, is_deleted
.add_field("name", FieldTemplate(base_type=str))
.add_field("sku", FieldTemplate(base_type=str))
.add_field("price", FieldTemplate(base_type=float, ge=0))
.build()
)
# Usage in queries
async def get_active_products(adapter):
return await adapter.find_many({"is_deleted": False})
Pattern 4: Embeddable Documents¶
For vector search and similarity operations:
from pydapter.fields import create_protocol_model, FieldTemplate
Document = create_protocol_model(
"Document",
"identifiable",
"temporal",
"embeddable", # Adds embedding field
title=FieldTemplate(base_type=str),
content=FieldTemplate(base_type=str),
metadata=FieldTemplate(
base_type=dict,
default_factory=dict,
description="Additional document metadata"
)
)
# Usage with vector adapters
async def search_similar(adapter, query_embedding):
return await adapter.similarity_search(
embedding=query_embedding,
limit=10
)
Pattern 5: Event Sourcing¶
For event-driven architectures, use the Event class directly:
from pydapter.protocols import Event
from pydantic import Field
# Extend the Event class for custom event types
class UserEvent(Event):
user_id: str = Field(..., description="User identifier")
action: str = Field(..., description="Action performed")
details: dict = Field(default_factory=dict, description="Additional details")
# Create events
login_event = UserEvent(
handler=lambda: None, # Required by Event
handler_arg=(),
handler_kwargs={},
event_type="user.login",
user_id="user123",
action="login",
content={"ip": "192.168.1.1", "user_agent": "..."},
request={"endpoint": "/api/login", "method": "POST"}
)
# The Event class includes all protocol behaviors:
# - IdentifiableMixin
# - TemporalMixin
# - EmbeddableMixin
# - InvokableMixin
# - CryptographicalMixin
Pattern 6: Custom Protocol Combinations¶
Mix protocols based on your needs:
# Cryptographically signed document
SignedDocument = create_protocol_model(
"SignedDocument",
"identifiable",
"temporal",
"cryptographical", # Adds sha256 field
document_type=FieldTemplate(base_type=str),
content=FieldTemplate(base_type=str),
signature=FieldTemplate(base_type=str)
)
# Executable task with tracking
Task = create_protocol_model(
"Task",
"identifiable",
"temporal",
"invokable", # Adds execution field
name=FieldTemplate(base_type=str),
parameters=FieldTemplate(base_type=dict, default_factory=dict),
status=FieldTemplate(base_type=str, default="pending")
)
Pattern 7: Adding Behavioral Methods to Protocol Models¶
To get both structural fields AND behavioral methods from protocols:
from pydapter.fields import FieldTemplate
from pydapter.protocols import (
IDENTIFIABLE, TEMPORAL,
create_protocol_model_class,
combine_with_mixins,
create_protocol_model
)
# Option 1: Use create_protocol_model_class (recommended)
User = create_protocol_model_class(
"User",
IDENTIFIABLE,
TEMPORAL,
username=FieldTemplate(base_type=str, max_length=50),
email=FieldTemplate(base_type=str)
)
# Option 2: Use combine_with_mixins
# First create structure
UserStructure = create_protocol_model(
"UserStructure",
IDENTIFIABLE,
TEMPORAL,
username=FieldTemplate(base_type=str, max_length=50),
email=FieldTemplate(base_type=str)
)
# Then add behaviors
User = combine_with_mixins(UserStructure, IDENTIFIABLE, TEMPORAL, name="User")
# Both options give you fields AND methods
user = User(username="john_doe", email="john@example.com")
user.update_timestamp() # Method from TemporalMixin
print(user.created_at) # Field from protocol
print(user.updated_at) # Updated by the method call
Pattern 8: Validation Patterns¶
Use pre-built validation patterns for common field types:
from pydapter.fields import (
create_pattern_template,
create_range_template,
ValidationPatterns,
DomainModelBuilder
)
UserProfile = (
DomainModelBuilder("UserProfile")
.with_entity_fields()
.add_field("username", create_pattern_template(
ValidationPatterns.USERNAME_ALPHANUMERIC,
description="Alphanumeric username"
))
.add_field("email", create_pattern_template(
ValidationPatterns.EMAIL,
description="User email address"
))
.add_field("age", create_range_template(
int,
ge=13,
le=120,
description="User age"
))
.add_field("website", create_pattern_template(
ValidationPatterns.HTTPS_URL,
description="Personal website",
default=None
).as_nullable())
.build()
)
Pattern 8: Adapter-Specific Metadata¶
Add metadata for adapter optimization:
from pydapter.fields import FieldTemplate, DomainModelBuilder
SearchableProduct = (
DomainModelBuilder("SearchableProduct")
.with_entity_fields()
.add_field("name", FieldTemplate(
base_type=str,
json_schema_extra={"db_index": True}
))
.add_field("description", FieldTemplate(
base_type=str,
json_schema_extra={"db_fulltext": True}
))
.add_field("tags", FieldTemplate(
base_type=list[str],
default_factory=list,
json_schema_extra={"db_index": True}
))
.add_field("embedding", FieldTemplate(
base_type=list[float],
default_factory=list,
json_schema_extra={
"vector_dim": 1536,
"index_type": "hnsw",
"distance_metric": "cosine"
}
))
.build()
)
Best Practices¶
1. Choose the Right Pattern¶
- Simple CRUD: Use
create_protocol_model
with "identifiable" and "temporal" - Audit Requirements: Add audit fields with
.with_audit_fields()
- Soft Delete: Use
.with_soft_delete()
for logical deletion - Event Sourcing: Use the "event" protocol for complete event tracking
2. Protocol Selection Guidelines¶
# Minimal entity
Model = create_protocol_model("Model", "identifiable")
# Standard entity with timestamps
Model = create_protocol_model("Model", "identifiable", "temporal")
# Searchable content
Model = create_protocol_model("Model", "identifiable", "temporal", "embeddable")
# Event with all tracking
Model = create_protocol_model("Model", "event")
3. Field Template Reuse¶
# Define common templates once
from pydapter.fields import FieldTemplate
# Reusable templates
MONEY = FieldTemplate(
base_type=float,
ge=0,
description="Monetary amount",
json_schema_extra={"format": "money", "decimal_places": 2}
)
STATUS = FieldTemplate(
base_type=str,
description="Status field",
default="active",
json_schema_extra={"enum": ["active", "inactive", "pending"]}
)
# Use across models
Invoice = create_protocol_model(
"Invoice",
"identifiable",
"temporal",
amount=MONEY,
status=STATUS
)
Payment = create_protocol_model(
"Payment",
"identifiable",
"temporal",
amount=MONEY,
status=STATUS.copy(default="pending")
)
4. Timezone Awareness¶
Always specify timezone awareness explicitly:
# Timezone-aware (recommended for distributed systems)
Model = create_protocol_model("Model", "temporal", timezone_aware=True)
# Naive datetime (only for single-timezone applications)
Model = create_protocol_model("Model", "temporal", timezone_aware=False)
5. Composing Complex Models¶
For complex requirements, combine approaches:
# Start with protocol model, then enhance
BaseOrder = create_protocol_model(
"BaseOrder",
"identifiable",
"temporal",
order_number=FieldTemplate(base_type=str)
)
# Extend with builder pattern
CompleteOrder = (
DomainModelBuilder("CompleteOrder")
.with_family({
"id": BaseOrder.model_fields["id"],
"created_at": BaseOrder.model_fields["created_at"],
"updated_at": BaseOrder.model_fields["updated_at"],
"order_number": BaseOrder.model_fields["order_number"]
})
.with_soft_delete()
.with_audit_fields()
.add_field("items", FieldTemplate(
base_type=list[dict],
default_factory=list
))
.build()
)
Migration Strategy¶
When migrating existing models:
- Identify which protocols your model should implement
- Use
create_protocol_model
to ensure protocol compliance - Add domain-specific fields as extra parameters
- Gradually refactor to use field families for consistency
# Before
class User(BaseModel):
id: UUID = Field(default_factory=uuid4)
created_at: datetime = Field(default_factory=datetime.utcnow)
username: str
email: str
# After
User = create_protocol_model(
"User",
"identifiable",
"temporal",
username=FieldTemplate(base_type=str),
email=FieldTemplate(base_type=str)
)
This approach ensures protocol compliance while maintaining flexibility for domain-specific needs.