Facets — Aquilia Documentation
Comprehensive guide and documentation for Facets in the Aquilia framework. View API reference, examples, and implementation patterns.
Docs / Contracts / Facets Facets Atomic field-level primitives of a Contract contract. Each Facet manages type coercion (cast), validation (seal), and output representation (mold). Base Facet Options from aquilia.contracts import Facet field = Facet( source="model_field", # read from distinct model attribute required=True, # fail CastFault if missing on inbound read_only=False, # exclude from inbound cast write_only=False, # exclude from outbound serialization default=None, # fallback value allow_null=False, # accept None allow_blank=False, # accept empty string (TextFacet only) validators=[], # additional validator callables ) Built-in Facets TextFacet Handles string properties with length boundaries and pattern matching. sku = TextFacet(max_length=50, pattern=r"^[A-Z0-9-]+$") IntFacet Coerces numeric strings/floats to integers. Validates min_value and max_value. Rejects booleans. quantity = IntFacet(min_value=1, max_value=99) Computed Derived read-only fields computed via a function or method. Can also use the @computed decorator. # Inline lambda computed facet full_name = Computed(lambda bp: f" ") # Method decorator pattern @computed def display_title(self) -> str: return self.instance.title.upper() Choice Constraint Restricts values to a specific set. Supports lists, dicts, or tuples: from aquilia.contracts import ChoiceFacet # List choices status = ChoiceFacet(choices=["draft", "published"]) # Dict choices (value -> description) priority = ChoiceFacet(choices= ) Facet Registry Reference Facet Python Target Description ))} Typed Primitives Added in v1.3.5. Four types that previously fell through to a permissive TextFacet or had no facet at all. BytesFacet Binary data over a JSON transport. Size constraints apply to the decoded length, which is what matters for memory. class UploadContract(Contract): payload = BytesFacet() # base64 (default) checksum = BytesFacet(encoding="hex") thumbnail = BytesFacet(max_length=64 * 1024) UploadContract(data= ) # validated_data: Always bound max_length on a client-facing binary field. Base64 expands roughly 33%, so a modest request body still decodes to a large allocation. PathFacet Filesystem paths validated as pathlib.PurePosixPath, so a payload validates identically regardless of server platform. class UploadContract(Contract): destination = PathFacet() UploadContract(data= ) # validated_data: # Rejected by default: # "/etc/passwd" -> Path must be relative # "../../etc/passwd" -> Path may not contain '..' segments # "a\\x00b" -> Path may not contain null bytes The defaults reject the two ways a client-supplied path escapes its root. Null bytes are refused unconditionally — they truncate at the OS layer, so a name passing an extension check can still open a different file. Relax with must_be_relative=False / allow_traversal=True only for paths that never originate from a request. SecretFacet Sensitive strings that never appear in output or tracebacks. write_only by default. class LoginContract(Contract): password = SecretFacet(min_length=8) secret = contract.validated_data["password"] repr(secret) # "Secret('**********')" str(secret) # "**********" secret.reveal() # "hunter2hunter2" if secret == stored_secret: # constant-time comparison ... Equality uses hmac.compare_digest, so comparing a submitted value against a stored one does not leak the shared-prefix length through timing. Masking defends against accidental disclosure — log lines, exception reports, debug pages — and is not a substitute for hashing or encryption at rest. Call .reveal() only at the point of use. MACAddressFacet Accepts colon, dash, and Cisco notations, normalizing at validation so downstream comparisons and database lookups do not each reimplement it. class DeviceContract(Contract): mac = MACAddressFacet() # "AA:BB:CC:DD:EE:FF" -> "aa:bb:cc:dd:ee:ff" # "aa-bb-cc-dd-ee-ff" -> "aa:bb:cc:dd:ee:ff" # "aabb.ccdd.eeff" -> "aa:bb:cc:dd:ee:ff" Annotation Routing These types resolve to the right facet from a plain annotation: import ipaddress, pathlib from aquilia.contracts.facets import Secret class DeviceContract(Contract): address: ipaddress.IPv4Address # IPFacet config_path: pathlib.Path # PathFacet api_key: Secret # SecretFacet payload: bytes # BytesFacet Integer Coercion class QuantityContract(Contract): qty = IntFacet() QuantityContract(data= ).is_sealed() # True — integral float QuantityContract(data= ).errors # Changed in v1.3.5. 3.9 was previously truncated to 3 while the string "3.9" was correctly rejected — the same logical input behaved differently depending on wire type. Silent truncation of a quantity or a price in cents is a data-integrity bug that surfaces far from its cause. NaN and Infinity are now rejected explicitly. Overview Projections )
Go to Homepage