CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-schematics

Python Data Structures for Humans - a library for data validation and transformation using structured models

Pending
Overview
Eval results
Files

dynamic-fields.mddocs/

Dynamic Fields

Advanced field functionality for computed properties, serializable fields, and dynamic field generation in Schematics. These features enable flexible data modeling with calculated values, custom serialization, and runtime field creation.

Capabilities

Serializable Fields

Create computed fields that appear in model serialization with custom calculation logic.

def serializable(func=None, **field_kwargs):
    """
    Decorator for creating serializable computed fields.
    
    Creates fields that compute their values dynamically and include
    the results in model export and serialization operations.
    
    Args:
        func (callable, optional): Function to compute field value
        **field_kwargs: Additional field options (type, required, etc.)
        
    Returns:
        Serializable field descriptor
    """

class Serializable:
    """
    Dynamic serializable field descriptor for computed properties.
    
    Enables creation of fields that compute values at export time
    based on other model data or external calculations.
    """
    
    def __init__(self, **kwargs):
        """
        Initialize serializable field.
        
        Args:
            **kwargs: Field configuration options
        """

Calculated Fields

Create fields with values calculated from other model fields.

def calculated(func, **field_kwargs):
    """
    Create calculated fields based on other model data.
    
    Calculated fields automatically compute their values when accessed
    based on the current state of other model fields.
    
    Args:
        func (callable): Function to calculate field value
        **field_kwargs: Additional field options
        
    Returns:
        Calculated field descriptor
    """

Dynamic Field Creation

Runtime field creation and model customization capabilities.

class DynamicModel(Model):
    """
    Model subclass supporting runtime field addition.
    
    Enables dynamic model schemas that can be modified at runtime
    based on configuration or external requirements.
    """
    
    @classmethod
    def add_field(cls, name, field_type):
        """
        Add field to model class at runtime.
        
        Args:
            name (str): Field name
            field_type (BaseType): Field type instance
        """

Usage Examples

Basic Serializable Fields

from schematics.models import Model
from schematics.types import StringType, IntType, DateTimeType, serializable
from datetime import datetime

class User(Model):
    first_name = StringType(required=True)
    last_name = StringType(required=True) 
    birth_year = IntType()
    created_at = DateTimeType(default=datetime.utcnow)
    
    @serializable
    def full_name(self):
        """Computed full name from first and last name."""
        return f"{self.first_name} {self.last_name}"
    
    @serializable
    def age(self):
        """Calculated age from birth year."""
        if self.birth_year:
            return datetime.now().year - self.birth_year
        return None
    
    @serializable
    def account_age_days(self):
        """Days since account creation."""
        if self.created_at:
            return (datetime.utcnow() - self.created_at).days
        return 0

# Usage
user = User({
    'first_name': 'John',
    'last_name': 'Doe', 
    'birth_year': 1990
})

# Computed fields available in serialization
user_data = user.to_primitive()
print(user_data)
# {
#     'first_name': 'John',
#     'last_name': 'Doe',
#     'birth_year': 1990,
#     'created_at': '2024-01-15T10:30:00.123456Z',
#     'full_name': 'John Doe',
#     'age': 34,
#     'account_age_days': 0
# }

Advanced Serializable Fields with Type Hints

from schematics.types import FloatType, ListType, StringType

class Product(Model):
    name = StringType(required=True)
    base_price = FloatType(required=True, min_value=0)
    tax_rate = FloatType(default=0.08, min_value=0, max_value=1)
    discount_percent = FloatType(default=0, min_value=0, max_value=100)
    tags = ListType(StringType())
    
    @serializable(type=FloatType())
    def tax_amount(self):
        """Calculate tax amount."""
        return self.base_price * self.tax_rate
    
    @serializable(type=FloatType())  
    def discount_amount(self):
        """Calculate discount amount."""
        return self.base_price * (self.discount_percent / 100)
    
    @serializable(type=FloatType())
    def final_price(self):
        """Calculate final price after tax and discount."""
        return self.base_price + self.tax_amount - self.discount_amount
    
    @serializable(type=StringType())
    def tag_summary(self):
        """Create summary of product tags."""
        if self.tags:
            return ', '.join(self.tags)
        return 'No tags'
    
    @serializable(type=StringType())
    def price_tier(self):
        """Determine price tier based on final price."""
        if self.final_price < 20:
            return 'Budget'
        elif self.final_price < 100:
            return 'Standard'
        else:
            return 'Premium'

# Usage
product = Product({
    'name': 'Wireless Headphones',
    'base_price': 99.99,
    'tax_rate': 0.08,
    'discount_percent': 15,
    'tags': ['electronics', 'audio', 'wireless']
})

print(product.to_primitive())
# Includes all computed fields: tax_amount, discount_amount, final_price, etc.

Calculated Fields for Data Aggregation

from schematics.types import ModelType, ListType, calculated

class OrderItem(Model):
    product_name = StringType(required=True)
    quantity = IntType(required=True, min_value=1)
    unit_price = FloatType(required=True, min_value=0)
    
    @serializable(type=FloatType())
    def line_total(self):
        return self.quantity * self.unit_price

class Order(Model):
    order_id = StringType(required=True)
    customer_name = StringType(required=True)
    items = ListType(ModelType(OrderItem), required=True, min_size=1)
    shipping_cost = FloatType(default=0, min_value=0)
    
    @calculated(type=FloatType())
    def subtotal(self):
        """Calculate subtotal from all line items."""
        return sum(item.line_total for item in self.items)
    
    @calculated(type=IntType())
    def total_items(self):
        """Count total quantity across all items."""
        return sum(item.quantity for item in self.items)
    
    @calculated(type=FloatType())
    def total_amount(self):
        """Calculate final total including shipping."""
        return self.subtotal + self.shipping_cost
    
    @serializable(type=StringType())
    def order_summary(self):
        """Generate human-readable order summary."""
        return f"Order {self.order_id}: {self.total_items} items, ${self.total_amount:.2f}"

# Usage
order = Order({
    'order_id': 'ORD-001',
    'customer_name': 'Jane Smith',
    'shipping_cost': 5.99,
    'items': [
        {'product_name': 'Widget A', 'quantity': 2, 'unit_price': 12.50},
        {'product_name': 'Widget B', 'quantity': 1, 'unit_price': 25.00}
    ]
})

print(f"Subtotal: ${order.subtotal:.2f}")     # $50.00
print(f"Total items: {order.total_items}")    # 3 
print(f"Final total: ${order.total_amount:.2f}") # $55.99
print(order.order_summary)                    # "Order ORD-001: 3 items, $55.99"

Dynamic Model Creation

def create_survey_model(questions):
    """
    Dynamically create a survey model based on question configuration.
    
    Args:
        questions (list): List of question dictionaries
        
    Returns:
        Model: Dynamically created survey model class
    """
    
    class SurveyResponse(Model):
        respondent_id = StringType(required=True)
        submitted_at = DateTimeType(default=datetime.utcnow)
        
        @serializable(type=IntType())
        def response_count(self):
            """Count non-empty responses."""
            count = 0
            for field_name, field in self._fields.items():
                if field_name not in ['respondent_id', 'submitted_at']:
                    value = getattr(self, field_name, None) 
                    if value is not None and value != '':
                        count += 1
            return count
    
    # Add dynamic fields based on questions
    for question in questions:
        field_name = question['name']
        field_type = question['type']
        
        if field_type == 'text':
            field = StringType(required=question.get('required', False))
        elif field_type == 'number':
            field = IntType(required=question.get('required', False))
        elif field_type == 'choice':
            field = StringType(
                required=question.get('required', False),
                choices=question.get('choices', [])
            )
        elif field_type == 'multiple_choice':
            field = ListType(StringType(choices=question.get('choices', [])))
        else:
            field = StringType()
        
        # Add field to model class
        setattr(SurveyResponse, field_name, field)
    
    return SurveyResponse

# Define survey questions
survey_questions = [
    {'name': 'satisfaction', 'type': 'choice', 'required': True,
     'choices': ['very_satisfied', 'satisfied', 'neutral', 'dissatisfied']},
    {'name': 'recommendation_score', 'type': 'number', 'required': True},
    {'name': 'feedback', 'type': 'text', 'required': False},
    {'name': 'interests', 'type': 'multiple_choice', 
     'choices': ['tech', 'sports', 'music', 'travel']}
]

# Create dynamic survey model
SurveyModel = create_survey_model(survey_questions)

# Use the dynamically created model
response = SurveyModel({
    'respondent_id': 'RESP-001',
    'satisfaction': 'satisfied',
    'recommendation_score': 8,
    'feedback': 'Great service!',
    'interests': ['tech', 'music']
})

response.validate()
print(f"Response count: {response.response_count}")  # 4
print(response.to_primitive())

Conditional Serializable Fields

class Report(Model):
    title = StringType(required=True)
    data = ListType(IntType(), required=True)
    include_statistics = BooleanType(default=False)
    include_chart = BooleanType(default=False)
    
    @serializable(type=FloatType())
    def average(self):
        """Calculate average of data points."""
        if self.data:
            return sum(self.data) / len(self.data)
        return 0
    
    @serializable(type=IntType())
    def data_count(self):
        """Count of data points."""
        return len(self.data) if self.data else 0
    
    @serializable
    def statistics(self):
        """Include detailed statistics if requested."""
        if not self.include_statistics:
            return None
            
        if not self.data:
            return None
            
        data_sorted = sorted(self.data)
        return {
            'min': min(data_sorted),
            'max': max(data_sorted),
            'median': data_sorted[len(data_sorted) // 2],
            'range': max(data_sorted) - min(data_sorted)
        }
    
    @serializable
    def chart_config(self):
        """Include chart configuration if requested."""
        if not self.include_chart:
            return None
            
        return {
            'type': 'line',
            'data': self.data,
            'title': self.title,
            'y_axis_label': 'Values'
        }

# Usage with different configurations
basic_report = Report({
    'title': 'Sales Data',
    'data': [100, 150, 120, 180, 200]
})

detailed_report = Report({
    'title': 'Sales Data',
    'data': [100, 150, 120, 180, 200],
    'include_statistics': True,
    'include_chart': True
})

basic_data = basic_report.to_primitive()
# Contains: title, data, average, data_count (statistics and chart_config are None)

detailed_data = detailed_report.to_primitive() 
# Contains: all basic fields plus statistics and chart_config objects

Install with Tessl CLI

npx tessl i tessl/pypi-schematics

docs

basic-types.md

compound-types.md

contrib-modules.md

dynamic-fields.md

exceptions.md

index.md

models.md

network-types.md

utilities.md

tile.json