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() supports string and deferred annotations, including Python 3.14's deferred evaluation behavior. Annotations are resolved against the module globals during Contract class construction. 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__" Definition errors are never downgraded In v1.4.0b5, a CastFault raised while Python evaluates a deferred facet annotation propagates from class creation. Aquilia still warns and skips introspection for unrelated resolution failures, but it does not suppress validation or security failures such as an unsafe regular-expression constraint. )
Go to Homepage