hack-house/.venv/lib/python3.12/site-packages/sanic_ext/extras/validation/schema.py
leetcrypt bb1d662ee1 chore: rename project coven → hack-house ⛧
Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"),
README, CLI strings, and branch (coven → hack-house). Gitea repo renamed
cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest +
golden-vector test still green, binary is now `hack-house`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 13:29:14 -07:00

134 lines
3.2 KiB
Python

import types
from dataclasses import MISSING, Field, is_dataclass
from inspect import isclass, signature
from typing import (
Any,
Literal,
Optional,
Union,
get_args,
get_origin,
get_type_hints,
)
from sanic_ext.utils.typing import is_attrs, is_generic, is_msgspec
from .check import Hint
try:
UnionType = types.UnionType # type: ignore
except AttributeError:
UnionType = type("UnionType", (), {})
try:
from attr import NOTHING, Attribute
except ModuleNotFoundError:
NOTHING = object() # type: ignore
Attribute = type("Attribute", (), {}) # type: ignore
try:
from msgspec.inspect import type_info as msgspec_type_info
except ModuleNotFoundError:
def msgspec_type_info(val):
pass
def make_schema(agg, item):
if type(item) in (bool, str, int, float):
return agg
if is_generic(item) and (args := get_args(item)):
for arg in args:
make_schema(agg, arg)
elif item.__name__ not in agg and (
is_dataclass(item) or is_attrs(item) or is_msgspec(item)
):
if is_dataclass(item):
fields = item.__dataclass_fields__
elif is_msgspec(item):
fields = {f.name: f.type for f in msgspec_type_info(item).fields}
else:
fields = {attr.name: attr for attr in item.__attrs_attrs__}
sig = signature(item)
hints = parse_hints(get_type_hints(item), fields)
agg[item.__name__] = {
"sig": sig,
"hints": hints,
}
for hint in hints.values():
make_schema(agg, hint.hint)
return agg
def parse_hints(
hints, fields: dict[str, Union[Field, Attribute]]
) -> dict[str, Hint]:
output: dict[str, Hint] = {
name: parse_hint(hint, fields.get(name))
for name, hint in hints.items()
}
return output
def parse_hint(hint, field: Optional[Union[Field, Attribute]] = None):
origin = None
literal = not isclass(hint)
nullable = False
typed = False
model = False
allowed: tuple[Any, ...] = tuple()
allow_missing = False
if field and (
(
isinstance(field, Field) and field.default_factory is not MISSING # type: ignore
)
or (isinstance(field, Attribute) and field.default is not NOTHING)
):
allow_missing = True
if is_dataclass(hint) or is_attrs(hint):
model = True
elif is_generic(hint):
typed = True
literal = False
origin = get_origin(hint)
args = get_args(hint)
nullable = origin in (Union, UnionType) and type(None) in args
if nullable:
allowed = tuple(
[
arg
for arg in args
if is_generic(arg) or not isinstance(None, arg)
]
)
elif origin is dict:
allowed = (args[1],)
elif (
origin is list
or origin is Literal
or origin is Union
or origin is UnionType
):
allowed = args
return Hint(
hint,
model,
literal,
typed,
nullable,
origin,
tuple([parse_hint(item, None) for item in allowed]),
allow_missing,
)