"""types — input types with their bounds, written once for every brick's use cases.
A use case's parameter annotations ARE its input contract: both doors build their schema and their
validation from them (erp_platform.bricks). An unbounded `str`, `int` or `Decimal` is a promise the
database cannot keep, and it broke in the CRM audit (2026-10-17) as a 500 instead of a refusal:
a quantity of 2**31 INTEGER overflow inside the engine
an amount of 1e20 NUMERIC(18,2) overflow
a bp_id of 23 digits BIGINT overflow
a 200 KB name the write refused by the engine
"not-a-date" as a date a ValueError in the use case
Each of those is the CALLER's mistake and deserves a 422 that says which field and why. These types make
it one through both doors, before the use case runs.
THE 64-BIT ID. `BigId` is a BIGINT key (bp_id): it is accepted as a number OR as a string of digits, and
the schema both doors publish says `type: string, format: int64` — a JavaScript client or an agent cannot
hold an integer past 2**53 as a number, and a rounded id names another customer. A brick that opts in
(`Brick(..., ids_as_strings=True)`) also RETURNS every `*_id` integer as a string (erp_platform.bricks).
"""
import re
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from typing import Annotated, Any
from pydantic import AfterValidator, BeforeValidator, Field, WithJsonSchema
INT64_MIN, INT64_MAX = -(2 ** 63), 2 ** 63 - 1
def _big_id(v: Any) -> int:
if isinstance(v, bool):
raise ValueError("an id is a whole number, not true/false")
if isinstance(v, int):
n = v
elif isinstance(v, str) and v.strip().lstrip("-").isdigit() and v.strip().isascii():
n = int(v.strip())
elif isinstance(v, float) and v.is_integer() and abs(v) < 2 ** 53:
n = int(v) # exact below 2**53; above it the digits were already lost
else:
raise ValueError("an id is a whole number, given as digits (a 64-bit integer)")
if not (INT64_MIN <= n <= INT64_MAX):
raise ValueError("an id is a 64-bit integer; this one has too many digits")
return n
BigId = Annotated[int, BeforeValidator(_big_id),
WithJsonSchema({"type": "string", "format": "int64", "pattern": r"^-?[0-9]{1,19}$",
"description": "a 64-bit id, as digits (a JSON number is accepted too)"})]
def text(max_length: int, *, min_length: int = 0):
"""A string of at most `max_length` characters (control characters are refused for every string by
the doors already). `text(200)` for a name, `text(5000)` for a note."""
return Annotated[str, Field(min_length=min_length, max_length=max_length)]
Name = text(200) # a name, a subject, a title, a key someone types
Email = text(254) # RFC 5321's limit on a path
Note = text(10_000) # free text a person writes
def _bounded_date(v: Any) -> str:
try:
d = date.fromisoformat(str(v).strip()[:10])
except ValueError:
raise ValueError(f"{v!r} is not a date (YYYY-MM-DD)") from None
if not (date(1900, 1, 1) <= d <= date(2199, 12, 31)):
raise ValueError(f"{d.isoformat()} is outside 1900-01-01 … 2199-12-31")
return d.isoformat()
def _bounded_time(v: Any) -> str:
s = str(v).strip()
try:
d = datetime.fromisoformat(s.replace("Z", "+00:00"))
except ValueError:
raise ValueError(f"{v!r} is not a time (ISO 8601)") from None
if not (1900 <= d.year <= 2199):
raise ValueError(f"{s} is outside the years 1900 … 2199")
return s
IsoDate = Annotated[str, Field(max_length=32), AfterValidator(_bounded_date),
WithJsonSchema({"type": "string", "format": "date"})]
IsoTime = Annotated[str, Field(max_length=64), AfterValidator(_bounded_time),
WithJsonSchema({"type": "string", "format": "date-time"})]
# NUMERIC(18,2): 16 digits before the point. Three places are let through for the 3-place currencies
# (KWD, BHD); erp_platform.money then refuses more places than the currency has.
AMOUNT_LIMIT = Decimal("1e15")
def _amount(v: Any) -> Decimal:
if isinstance(v, bool):
raise ValueError("an amount is a number")
try:
d = v if isinstance(v, Decimal) else Decimal(str(v).strip())
except (InvalidOperation, ValueError):
raise ValueError(f"{v!r} is not an amount") from None
if not d.is_finite():
raise ValueError("an amount is a finite number")
if abs(d) >= AMOUNT_LIMIT:
raise ValueError("an amount is below 1,000,000,000,000,000")
if d.as_tuple().exponent < -3:
raise ValueError(f"{d} has more decimal places than any currency")
return d
Amount = Annotated[Decimal, BeforeValidator(_amount),
WithJsonSchema({"type": "string", "pattern": r"^-?[0-9]{1,16}(\.[0-9]{1,3})?$",
"description": "an exact decimal, as a string (\"1250.00\")"})]
Quantity = Annotated[int, Field(ge=1, le=1_000_000)]
Percent = Annotated[Decimal, Field(ge=0, le=100)]
Limit = Annotated[int, Field(ge=1, le=1000)]
# Control characters: refused in every string a door receives (erp_platform.bricks), tab/newline/CR excepted.
# A NUL cannot be stored at all (the engine's 500 in the audit); the rest are never a person's text.
CONTROL = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
def control_character(value: Any, path: str = "") -> str:
"""The first field ("lines.2.description") holding a control character, or "" when none does."""
if isinstance(value, str):
return (path or "input") if CONTROL.search(value) else ""
if isinstance(value, dict):
for k, v in value.items():
found = control_character(v, f"{path}.{k}" if path else str(k))
if found:
return found
return ""
if isinstance(value, (list, tuple)):
for i, v in enumerate(value):
found = control_character(v, f"{path}.{i}" if path else str(i))
if found:
return found
return ""