Annotations — Aquilia Documentation
Comprehensive guide and documentation for Annotations in the Aquilia framework. View API reference, examples, and implementation patterns.
Contracts / Annotations & Field() Annotations & Field() As an alternative to explicit Facet declarations, Aquilia supports type-annotation-driven Contracts using the Field descriptor. Write Pythonic type hints and let the metaclass derive the correct Facets automatically. Two Declaration Styles Explicit Facets Full control, more verbose class UserBP(Contract): name = TextFacet(max_length=100) age = IntFacet(min_value=0) email = EmailFacet() Type Annotations Pythonic, concise, auto-derived class UserBP(Contract): name: str = Field(max_length=100) age: int = Field(ge=0) email: str = Field() Both styles produce identical Contract behavior. You can even mix them in the same Contract — explicit Facets take priority over annotation-derived ones. The Field() Descriptor Field is a constraint descriptor that decorates type annotations with validation rules: from aquilia.contracts import Contract, Field class ProductContract(Contract): # All Field() options: name: str = Field( default=None, # Default value if not provided required=True, # Is this field required in input? read_only=False, # Exclude from input, include in output write_only=False, # Include in input, exclude from output allow_null=False, # Allow None values # Numeric constraints (maps to min_value/max_value) ge=0, # Greater than or equal to le=100, # Less than or equal to gt=None, # Greater than (strict) lt=None, # Less than (strict) # String constraints min_length=None, # Minimum string length max_length=200, # Maximum string length pattern=None, # Regex pattern # Collection constraints min_items=None, # Minimum list items max_items=None, # Maximum list items # Choice constraint choices=None, # Allowed values (list/dict/tuple) # Decimal constraints max_digits=None, # Total digits (DecimalFacet) decimal_places=None, # Decimal places (DecimalFacet) # Source mapping alias=None, # Alternative name in input JSON ) class Spec: model = Product fields = "__all__" Type → Facet Mapping The introspect_annotations() system maps Python type annotations to the appropriate Facet class: Type Annotation Derived Facet , , , , , , , , , , , , , , , , , , , ].map((row, i) => ( ))} Practical Examples User Registration from aquilia.contracts import Contract, Field from datetime import date class RegisterContract(Contract): username: str = Field(min_length=3, max_length=30) email: str = Field(required=True) # Becomes EmailFacet? No — str → TextFacet password: str = Field(min_length=8, write_only=True) birth_date: date = Field(required=False) age: int = Field(ge=13, le=120, required=False) class Spec: model = User fields = ["username", "email", "password", "birth_date"] E-Commerce Order from aquilia.contracts import Contract, Field from decimal import Decimal from uuid import UUID from typing import Optional class OrderItemContract(Contract): product_id: int = Field(required=True) quantity: int = Field(ge=1, le=999) unit_price: Decimal = Field(max_digits=10, decimal_places=2) note: Optional[str] = Field(max_length=500) # allow_null=True auto-set class Spec: model = OrderItem fields = "__all__" class OrderContract(Contract): reference: UUID = Field(read_only=True) customer_name: str = Field(max_length=200) items: list[OrderItemContract] = Field(min_items=1) # Nested Contract total: Decimal = Field(max_digits=12, decimal_places=2, read_only=True) status: str = Field(choices=["pending", "confirmed", "shipped", "delivered"]) class Spec: model = Order fields = "__all__" read_only_fields = ["reference", "total"] The @computed Decorator Mark methods as computed output fields using @computed. These fields appear in output but never accept input: from aquilia.contracts import Contract, Field, computed class UserContract(Contract): first_name: str = Field(max_length=50) last_name: str = Field(max_length=50) email: str = Field() avatar_url: str = Field(read_only=True) class Spec: model = User fields = ["first_name", "last_name", "email", "avatar_url"] @computed def full_name(self, instance): """Computed from first + last name.""" return f" " @computed def initials(self, instance): """First letter of each name part.""" return "".join( part[0].upper() for part in f" ".split() ) @computed def gravatar_url(self, instance): """Generate gravatar URL from email.""" import hashlib email_hash = hashlib.md5(instance.email.lower().encode()).hexdigest() return f"https://gravatar.com/avatar/ " # Output: # Nested Contracts via Annotations When you annotate a field with another Contract class, Aquilia creates a NestedContractFacet that delegates validation to the nested Contract: class AddressContract(Contract): street: str = Field(max_length=200) city: str = Field(max_length=100) zip_code: str = Field(max_length=20) country: str = Field(max_length=100, default="US") class Spec: model = Address fields = "__all__" class CompanyContract(Contract): name: str = Field(max_length=200) # Single nested Contract headquarters: AddressContract = Field(required=True) # List of nested Contracts offices: list[AddressContract] = Field(required=False) class Spec: model = Company fields = "__all__" # Input: # , # "offices": [ # , # ] # } # Each nested object is validated through AddressContract.is_sealed() Lazy Forward References For circular references, use string annotations. Aquilia resolves them lazily from the Contract registry: from __future__ import annotations # Enable PEP 563 class DepartmentContract(Contract): name: str = Field(max_length=100) # Forward reference — EmployeeContract not yet defined manager: "EmployeeContract" = Field(required=False) employees: list["EmployeeContract"] = Field(required=False) class Spec: model = Department fields = "__all__" class EmployeeContract(Contract): name: str = Field(max_length=100) department: DepartmentContract = Field() # Direct reference (already defined) class Spec: model = Employee fields = "__all__" # Both resolve correctly at runtime via _contract_registry Optional & Union Types from typing import Optional, Union class FlexibleContract(Contract): # Optional — allows null nickname: Optional[str] = Field(max_length=50) # Same as: str | None = Field(...) # Produces: TextFacet(allow_null=True, max_length=50) # Union — polymorphic identifier: str | int = Field() # Produces: PolymorphicFacet(candidates=[TextFacet(), IntFacet()]) # Tries str first, then int class Spec: model = Flexible fields = "__all__" PEP 563 / 649 Support Aquilia's introspect_annotations() fully supports deferred evaluation of annotations via from __future__ import annotations (PEP 563). String annotations are resolved against the module's globals at Contract class creation time. from __future__ import annotations # All annotations become strings from datetime import datetime from decimal import Decimal class InvoiceContract(Contract): # These are string annotations at parse time, # resolved to actual types by introspect_annotations() amount: Decimal = Field(max_digits=10, decimal_places=2) issued_at: datetime = Field(read_only=True) notes: str | None = Field(max_length=500) class Spec: model = Invoice fields = "__all__" )
Go to Homepage