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>
This commit is contained in:
leetcrypt
2026-05-30 13:29:14 -07:00
parent 82a04f3e12
commit bb1d662ee1
2730 changed files with 712933 additions and 46 deletions
@@ -0,0 +1,159 @@
import sys
from dataclasses import dataclass, field
from typing import Literal, Optional, Union
@dataclass
class ModelStr:
foo: str
@dataclass
class ModelInt:
foo: int
@dataclass
class ModelFloat:
foo: float
@dataclass
class ModelBool:
foo: bool
@dataclass
class ModelOptionalStr:
foo: Optional[str]
@dataclass
class ModelUnion:
foo: Union[int, float]
@dataclass
class ModelUnionModels:
foo: Union[ModelInt, ModelFloat]
@dataclass
class ModelUnionStrInt:
foo: Union[str, int]
@dataclass
class ModelUnionIntStr:
foo: Union[int, str]
@dataclass
class ModelOptionalUnionStrInt:
foo: Optional[Union[str, int]]
@dataclass
class ModelOptionalUnionIntStr:
foo: Optional[Union[int, str]]
@dataclass
class ModelListStr:
foo: list[str]
@dataclass
class ModelListModel:
foo: list[ModelStr]
@dataclass
class ModelOptionalList:
foo: Optional[list[str]]
@dataclass
class ModelListUnion:
foo: list[Union[int, float]]
@dataclass
class ModelOptionalListUnion:
foo: Optional[list[Union[int, float]]]
@dataclass
class ModelModel:
foo: ModelStr
@dataclass
class ModelOptionalModel:
foo: Optional[ModelStr]
@dataclass
class ModelDictStr:
foo: dict[str, str]
@dataclass
class ModelDictModel:
foo: dict[str, ModelStr]
@dataclass
class ModelOptionalDict:
foo: Optional[dict[str, str]]
@dataclass
class ModelDictUnion:
foo: dict[str, Union[int, float]]
@dataclass
class ModelOptionalDictUnion:
foo: Optional[dict[str, Union[int, float]]]
@dataclass
class ModelSingleLiteral:
foo: Literal[True]
@dataclass
class ModelMultipleLiteral:
foo: Literal[True, "y", "Y", 1]
@dataclass
class ModelOptionalSingleLiteral:
foo: Optional[Literal[True]]
@dataclass
class ModelOptionalMultipleLiteral:
foo: Optional[Literal[True, "y", "Y", 1]]
@dataclass
class ModelListStrWithDefaultFactory:
foo: list[str] = field(default_factory=list)
if sys.version_info > (3, 10):
@dataclass
class ModelUnionTypeStrNone:
foo: str | None
@dataclass
class ModelUnionTypeStrIntNone:
foo: str | int | None
@dataclass
class ModelUnionTypeStrInt:
foo: str | int
@@ -0,0 +1,24 @@
from sys import version_info
from typing import Any
import pytest
from sanic_ext.extras.validation.schema import parse_hint
@pytest.mark.skipif(version_info < (3, 9), reason="Not needed on 3.8")
def test_parse_generic_list():
hint_1 = parse_hint(list[int])
hint_2 = parse_hint(list[int])
assert hint_1.origin == hint_2.origin
assert hint_1.allowed == hint_2.allowed
@pytest.mark.skipif(version_info < (3, 9), reason="Not needed on 3.8")
def test_parse_generic_dict():
hint_1 = parse_hint(dict[str, Any])
hint_2 = parse_hint(dict[str, Any])
assert hint_1.origin == hint_2.origin
assert hint_1.allowed == hint_2.allowed
@@ -0,0 +1,43 @@
from unittest.mock import Mock
import pytest
from sanic import Sanic
from sanic.compat import Header
from sanic.response import json
from sanic_testing.reusable import ReusableClient
from sanic_ext import CountedRequest
@pytest.fixture(autouse=True)
def reset_counter():
yield
CountedRequest.reset_count()
def test_counter_increments(app: Sanic):
app.request_class = CountedRequest
@app.get("/")
async def handler(request: CountedRequest):
return json({"count": request.count})
@app.get("/info")
async def info(request: CountedRequest):
return json({"state": request.app.m.state})
with ReusableClient(app) as client:
for i in range(1, 10):
_, response = client.get("/")
assert response.json["count"] == i
def test_counter_increment_on_state(app: Sanic):
mock = Mock()
mock.state = {}
app.multiplexer = mock
for i in range(1, 10):
CountedRequest(b"/", Header({}), "", "", Mock(), app)
assert CountedRequest.count == i
@@ -0,0 +1,70 @@
from sanic import text
from sanic.response import json
from sanic_ext import serializer
def test_serializer_with_builtin(app):
@app.get("/")
@serializer(text)
async def handler(request):
return "hello"
@app.get("/201")
@serializer(text, status=201)
async def handler_201(request):
return "hello"
_, response = app.test_client.get("/")
assert response.status_code == 200
assert response.text == "hello"
assert response.content_type == "text/plain; charset=utf-8"
_, response = app.test_client.get("/201")
assert response.status_code == 201
assert response.content_type == "text/plain; charset=utf-8"
def test_serializer_with_custom(app):
def custom(message, status):
return json({"message": message}, status=status)
@app.get("/")
@serializer(custom)
async def handler(request):
return "hello"
@app.get("/201")
@serializer(custom, status=201)
async def handler_201(request):
return "hello"
_, response = app.test_client.get("/")
assert response.status_code == 200
assert response.content_type == "application/json"
assert response.json["message"] == "hello"
_, response = app.test_client.get("/201")
assert response.status_code == 201
assert response.content_type == "application/json"
def test_serializer_with_params(app):
def message(retval, request, action, status):
return json(
{
"request_id": str(request.id),
"action": action,
"message": retval,
},
status=status,
)
@app.get("/<action>")
@serializer(message)
async def do_action(request, action: str):
return "This is a message"
_, response = app.test_client.get("/this")
assert response.status_code == 200
assert response.json["action"] == "this"
@@ -0,0 +1,100 @@
from sys import version_info
from typing import Literal, Union
import pytest
from sanic import text
from sanic_ext import validate
def _dataclass_spec(annotation):
from dataclasses import dataclass
@dataclass
class Spec:
name: annotation
return Spec
def _attrs_spec(annotation):
import attrs
@attrs.define
class Spec:
name: annotation
return Spec
def _msgspec_spec(annotation):
from msgspec import Struct
class Spec(Struct):
name: annotation
return Spec
def _pydantic_spec(annotation):
from pydantic.dataclasses import dataclass as pydataclass
@pydataclass
class Spec:
name: annotation
return Spec
@pytest.mark.parametrize(
"annotation",
(
(
Literal["foo"],
Literal["foo", "bar"],
Union[Literal["foo"], Literal["bar"]],
)
),
)
@pytest.mark.parametrize(
"spec_builder",
(
_dataclass_spec,
_attrs_spec,
_msgspec_spec,
_pydantic_spec,
),
)
def test_literal(app, annotation, spec_builder):
Spec = spec_builder(annotation)
@app.get("/")
@validate(query=Spec)
def route(_, query: Spec):
return text(query.name)
_, response = app.test_client.get("", params={"name": "foo"})
assert response.text == "foo"
@pytest.mark.skipif(version_info < (3, 10), reason="Not needed on 3.10")
@pytest.mark.parametrize(
"spec_builder",
(
_dataclass_spec,
_attrs_spec,
_msgspec_spec,
_pydantic_spec,
),
)
def test_literal_3_10(app, spec_builder):
Spec = spec_builder(Literal["foo"] | Literal["bar"])
@app.get("/")
@validate(query=Spec)
def route(_, query: Spec):
return text(query.name)
_, response = app.test_client.get("", params={"name": "foo"})
assert response.text == "foo"
@@ -0,0 +1,112 @@
import attrs
from sanic import json
from sanic.views import HTTPMethodView
from sanic_ext import validate
SNOOPY_DATA = {"name": "Snoopy", "alter_ego": ["Flying Ace", "Joe Cool"]}
def test_validate_json(app):
@attrs.define
class Pet:
name: str
alter_ego: list[str]
@app.post("/function")
@validate(json=Pet)
async def handler(_, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(json=Pet)]
async def post(self, _, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
_, response = app.test_client.post("/function", json=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
_, response = app.test_client.post("/method", json=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
def test_validate_form(app):
@attrs.define
class Pet:
name: str
alter_ego: list[str]
@app.post("/function")
@validate(form=Pet)
async def handler(_, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(form=Pet)]
async def post(self, _, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
_, response = app.test_client.post("/function", data=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
_, response = app.test_client.post("/method", data=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
def test_validate_query(app):
@attrs.define
class Search:
q: str
@app.get("/function")
@validate(query=Search)
async def handler(_, query: Search):
return json({"q": query.q, "is_search": isinstance(query, Search)})
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(query=Search)]
async def get(self, _, query: Search):
return json({"q": query.q, "is_search": isinstance(query, Search)})
_, response = app.test_client.get("/function", params={"q": "Snoopy"})
assert response.status == 200
assert response.json["is_search"]
assert response.json["q"] == "Snoopy"
_, response = app.test_client.get("/method", params={"q": "Snoopy"})
assert response.status == 200
assert response.json["is_search"]
assert response.json["q"] == "Snoopy"
@@ -0,0 +1,410 @@
import sys
from dataclasses import dataclass
from typing import Optional
import pytest
from sanic import json
from sanic.views import HTTPMethodView
from sanic_ext import validate
from sanic_ext.extras.validation.check import check_data
from sanic_ext.extras.validation.schema import make_schema, parse_hint
from . import __models__ as models
SNOOPY_DATA = {"name": "Snoopy", "alter_ego": ["Flying Ace", "Joe Cool"]}
def test_schema():
@dataclass
class Pet:
name: str
@dataclass
class Person:
name: str
age: int
pets: Optional[list[Pet]]
schema = make_schema({}, Person)
assert "Person" in schema
assert schema["Person"]["hints"]["name"] == parse_hint(str)
assert schema["Person"]["hints"]["age"] == parse_hint(int)
assert schema["Person"]["hints"]["pets"] == parse_hint(Optional[list[Pet]])
assert "Pet" in schema
assert schema["Pet"]["hints"]["name"] == parse_hint(str)
def test_should_hydrate():
@dataclass
class Pet:
name: str
@dataclass
class Person:
name: str
age: int
pets: list[Pet]
data = {"name": "Charlie Brown", "age": 8, "pets": [{"name": "Snoopy"}]}
schema = make_schema({}, Person)
cb = check_data(Person, data, schema)
assert cb.name == "Charlie Brown"
assert cb.age == 8
assert cb.pets[0].name == "Snoopy"
@pytest.mark.parametrize(
"data",
(
{"name": "Charlie Brown", "age": 8, "pets": {"name": "Snoopy"}},
{"name": "Charlie Brown", "age": 8, "pets": [{"name": 123}]},
{"name": "Charlie Brown", "age": 8, "pets": [123]},
{"name": "Charlie Brown", "age": 8, "pets": 123},
{"name": "Charlie Brown", "age": "8", "pets": {"name": "Snoopy"}},
{"name": True, "age": 8, "pets": {"name": "Snoopy"}},
),
)
def test_should_not_hydrate(data):
@dataclass
class Pet:
name: str
@dataclass
class Person:
name: str
age: int
pets: list[Pet]
schema = make_schema({}, Person)
with pytest.raises(TypeError):
check_data(Person, data, schema)
@pytest.mark.parametrize(
"model,okay,data",
(
(models.ModelStr, True, {"foo": "bar"}),
(models.ModelStr, False, {"foo": 1}),
(models.ModelStr, False, {"foo": True}),
(models.ModelStr, False, {"foo": ["bar"]}),
(models.ModelStr, False, {"bar": "bar"}),
(models.ModelStr, False, {"foo": None}),
(models.ModelStr, False, 123),
(models.ModelInt, True, {"foo": 1}),
(models.ModelInt, True, {"foo": True}),
(models.ModelInt, False, {"foo": "1"}),
(models.ModelInt, False, {"foo": 1.1}),
(models.ModelInt, False, {"foo": None}),
(models.ModelFloat, True, {"foo": 1.1}),
(models.ModelFloat, False, {"foo": 1}),
(models.ModelFloat, False, {"foo": "1.1"}),
(models.ModelFloat, False, {"foo": None}),
(models.ModelBool, True, {"foo": True}),
(models.ModelBool, True, {"foo": False}),
(models.ModelBool, False, {"foo": 1}),
(models.ModelBool, False, {"foo": 0}),
(models.ModelBool, False, {"foo": 2}),
(models.ModelBool, False, {"foo": "True"}),
(models.ModelBool, False, {"foo": None}),
(models.ModelOptionalStr, True, {"foo": "bar"}),
(models.ModelOptionalStr, True, {"foo": None}),
(models.ModelOptionalStr, False, {"foo": 0}),
(models.ModelUnion, True, {"foo": 1}),
(models.ModelUnion, True, {"foo": 1.1}),
(models.ModelUnion, False, {"foo": "1.1"}),
(models.ModelUnion, False, {"foo": None}),
(models.ModelUnionModels, True, {"foo": {"foo": 1}}),
(models.ModelUnionModels, True, {"foo": {"foo": 1.1}}),
(models.ModelUnionModels, False, {"foo": {"foo": "1.1"}}),
(models.ModelUnionModels, False, {"foo": 1}),
(models.ModelUnionModels, False, {"foo": 1.1}),
(models.ModelUnionModels, False, {"foo": None}),
(models.ModelUnionStrInt, True, {"foo": "1"}),
(models.ModelUnionStrInt, True, {"foo": "1q"}),
(models.ModelUnionStrInt, True, {"foo": 1}),
(models.ModelUnionStrInt, False, {"foo": 1.1}),
(models.ModelUnionStrInt, False, {"foo": None}),
(models.ModelUnionIntStr, True, {"foo": "1"}),
(models.ModelUnionIntStr, True, {"foo": "1q"}),
(models.ModelUnionIntStr, True, {"foo": 1}),
(models.ModelUnionIntStr, False, {"foo": 1.1}),
(models.ModelUnionIntStr, False, {"foo": None}),
(models.ModelOptionalUnionStrInt, True, {"foo": "1"}),
(models.ModelOptionalUnionStrInt, True, {"foo": "1q"}),
(models.ModelOptionalUnionStrInt, True, {"foo": 1}),
(models.ModelOptionalUnionStrInt, False, {"foo": 1.1}),
(models.ModelOptionalUnionStrInt, True, {"foo": None}),
(models.ModelOptionalUnionIntStr, True, {"foo": "1"}),
(models.ModelOptionalUnionIntStr, True, {"foo": "1q"}),
(models.ModelOptionalUnionIntStr, True, {"foo": 1}),
(models.ModelOptionalUnionIntStr, False, {"foo": 1.1}),
(models.ModelOptionalUnionIntStr, True, {"foo": None}),
(models.ModelListStr, True, {"foo": ["bar"]}),
(models.ModelListStr, True, {"foo": ["one", "two"]}),
(models.ModelListStr, False, {"foo": "bar"}),
(models.ModelListStr, False, {"foo": ["one", 2]}),
(models.ModelListStr, False, {"foo": ["one", None]}),
(models.ModelListStr, False, {"foo": None}),
(models.ModelListModel, True, {"foo": [{"foo": "bar"}]}),
(
models.ModelListModel,
True,
{"foo": [{"foo": "one"}, {"foo": "two"}]},
),
(models.ModelListModel, False, {"foo": {"foo": "bar"}}),
(models.ModelListModel, False, {"foo": [{"foo": "bar"}, 2]}),
(models.ModelListModel, False, {"foo": [{"foo": "bar"}, None]}),
(models.ModelListModel, False, {"foo": None}),
(models.ModelOptionalList, True, {"foo": None}),
(models.ModelOptionalList, True, {"foo": ["bar"]}),
(models.ModelOptionalList, False, {"foo": [1]}),
(models.ModelOptionalList, False, {"foo": [None]}),
(models.ModelListUnion, True, {"foo": [1]}),
(models.ModelListUnion, True, {"foo": [1.1]}),
(models.ModelListUnion, True, {"foo": [1, 1.1]}),
(models.ModelListUnion, False, {"foo": [1, 1.1, "one"]}),
(models.ModelListUnion, False, {"foo": [1, 1.1, None]}),
(models.ModelListUnion, False, {"foo": 1}),
(models.ModelListUnion, False, {"foo": 1.1}),
(models.ModelListUnion, False, {"foo": None}),
(models.ModelOptionalListUnion, True, {"foo": [1]}),
(models.ModelOptionalListUnion, True, {"foo": [1.1]}),
(models.ModelOptionalListUnion, True, {"foo": [1, 1.1]}),
(models.ModelOptionalListUnion, True, {"foo": None}),
(models.ModelOptionalListUnion, False, {"foo": [1, 1.1, "one"]}),
(models.ModelOptionalListUnion, False, {"foo": [1, 1.1, None]}),
(models.ModelOptionalListUnion, False, {"foo": 1}),
(models.ModelOptionalListUnion, False, {"foo": 1.1}),
(models.ModelModel, True, {"foo": {"foo": "one"}}),
(models.ModelModel, False, {"foo": {"foo": 1}}),
(models.ModelModel, False, {"foo": {"foo": None}}),
(models.ModelModel, False, {"foo": "one"}),
(models.ModelModel, False, {"foo": None}),
(models.ModelOptionalModel, True, {"foo": {"foo": "one"}}),
(models.ModelOptionalModel, True, {"foo": None}),
(models.ModelOptionalModel, False, {"foo": {"foo": 1}}),
(models.ModelOptionalModel, False, {"foo": {"foo": None}}),
(models.ModelOptionalModel, False, {"foo": "one"}),
(models.ModelDictStr, True, {"foo": {"foo": "one"}}),
(models.ModelDictStr, False, {"foo": {"foo": 1}}),
(models.ModelDictStr, False, {"foo": {"foo": None}}),
(models.ModelDictStr, False, {"foo": "one"}),
(models.ModelDictStr, False, {"foo": None}),
(models.ModelDictModel, True, {"foo": {"foo": {"foo": "one"}}}),
(models.ModelDictModel, False, {"foo": {"foo": {"foo": 1}}}),
(models.ModelDictModel, False, {"foo": {"foo": 1}}),
(models.ModelDictModel, False, {"foo": {"foo": None}}),
(models.ModelDictModel, False, {"foo": "one"}),
(models.ModelDictModel, False, {"foo": None}),
(models.ModelOptionalDict, True, {"foo": {"foo": "one"}}),
(models.ModelOptionalDict, True, {"foo": None}),
(models.ModelOptionalDict, False, {"foo": {"foo": 1}}),
(models.ModelOptionalDict, False, {"foo": {"foo": None}}),
(models.ModelOptionalDict, False, {"foo": "one"}),
(models.ModelDictUnion, True, {"foo": {"foo": 1}}),
(models.ModelDictUnion, True, {"foo": {"foo": 1.1}}),
(models.ModelDictUnion, False, {"foo": {"foo": "one"}}),
(models.ModelDictUnion, False, {"foo": {"foo": None}}),
(models.ModelDictUnion, False, {"foo": "one"}),
(models.ModelDictUnion, False, {"foo": 1}),
(models.ModelDictUnion, False, {"foo": 1.1}),
(models.ModelDictUnion, False, {"foo": None}),
(models.ModelOptionalDictUnion, True, {"foo": {"foo": 1}}),
(models.ModelOptionalDictUnion, True, {"foo": {"foo": 1.1}}),
(models.ModelOptionalDictUnion, True, {"foo": None}),
(models.ModelOptionalDictUnion, False, {"foo": {"foo": "one"}}),
(models.ModelOptionalDictUnion, False, {"foo": {"foo": None}}),
(models.ModelOptionalDictUnion, False, {"foo": "one"}),
(models.ModelOptionalDictUnion, False, {"foo": 1}),
(models.ModelOptionalDictUnion, False, {"foo": 1.1}),
(models.ModelSingleLiteral, True, {"foo": True}),
(models.ModelSingleLiteral, False, {"foo": False}),
(models.ModelSingleLiteral, False, {"foo": "True"}),
(models.ModelSingleLiteral, False, {"foo": None}),
(models.ModelOptionalSingleLiteral, True, {"foo": True}),
(models.ModelOptionalSingleLiteral, True, {"foo": None}),
(models.ModelOptionalSingleLiteral, False, {"foo": False}),
(models.ModelOptionalSingleLiteral, False, {"foo": "True"}),
(models.ModelOptionalMultipleLiteral, True, {"foo": True}),
(models.ModelOptionalMultipleLiteral, True, {"foo": 1}),
(models.ModelOptionalMultipleLiteral, True, {"foo": "y"}),
(models.ModelOptionalMultipleLiteral, True, {"foo": "Y"}),
(models.ModelOptionalMultipleLiteral, True, {"foo": None}),
(models.ModelOptionalMultipleLiteral, False, {"foo": "n"}),
(models.ModelOptionalMultipleLiteral, False, {"foo": False}),
(models.ModelListStrWithDefaultFactory, True, {}),
(models.ModelListStrWithDefaultFactory, True, {"foo": ["bar"]}),
(models.ModelListStrWithDefaultFactory, True, {"foo": []}),
(models.ModelListStrWithDefaultFactory, False, {"foo": [1]}),
(models.ModelListStrWithDefaultFactory, False, {"foo": None}),
),
)
def test_modeling(model, okay, data):
schema = make_schema({}, model)
if okay:
check_data(model, data, schema)
else:
with pytest.raises(TypeError):
check_data(model, data, schema)
@pytest.mark.skipif(
sys.version_info < (3, 10), reason="UnionType added in 3.10"
)
def test_modeling_union_type_ModelUnionTypeStrNone():
schema = make_schema({}, models.ModelUnionTypeStrNone)
check_data(models.ModelUnionTypeStrNone, {"foo": "bar"}, schema)
check_data(models.ModelUnionTypeStrNone, {"foo": None}, schema)
with pytest.raises(TypeError):
check_data(models.ModelUnionTypeStrNone, {"foo": 0}, schema)
@pytest.mark.skipif(
sys.version_info < (3, 10), reason="UnionType added in 3.10"
)
def test_modeling_union_type_ModelUnionTypeStrIntNone():
schema = make_schema({}, models.ModelUnionTypeStrIntNone)
check_data(models.ModelUnionTypeStrIntNone, {"foo": "1"}, schema)
check_data(models.ModelUnionTypeStrIntNone, {"foo": "bar"}, schema)
check_data(models.ModelUnionTypeStrIntNone, {"foo": None}, schema)
check_data(models.ModelUnionTypeStrIntNone, {"foo": 1}, schema)
check_data(models.ModelUnionTypeStrIntNone, {"foo": 0}, schema)
with pytest.raises(TypeError):
check_data(models.ModelUnionTypeStrIntNone, {"foo": 1.1}, schema)
@pytest.mark.skipif(
sys.version_info < (3, 10), reason="UnionType added in 3.10"
)
def test_modeling_union_type_ModelUnionTypeStrInt():
schema = make_schema({}, models.ModelUnionTypeStrInt)
check_data(models.ModelUnionTypeStrInt, {"foo": "1"}, schema)
check_data(models.ModelUnionTypeStrInt, {"foo": "bar"}, schema)
check_data(models.ModelUnionTypeStrInt, {"foo": 1}, schema)
check_data(models.ModelUnionTypeStrInt, {"foo": 0}, schema)
with pytest.raises(TypeError):
check_data(models.ModelUnionTypeStrInt, {"foo": None}, schema)
with pytest.raises(TypeError):
check_data(models.ModelUnionTypeStrInt, {"foo": 1.1}, schema)
def test_validate_json(app):
@dataclass
class Pet:
name: str
alter_ego: list[str]
@app.post("/function")
@validate(json=Pet)
async def handler(_, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(json=Pet)]
async def post(self, _, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
_, response = app.test_client.post("/function", json=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
_, response = app.test_client.post("/method", json=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
def test_validate_form(app):
@dataclass
class Pet:
name: str
alter_ego: list[str]
description: Optional[str] = None
@app.post("/function")
@validate(form=Pet)
async def handler(_, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
"description": body.description if body.description else "",
}
)
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(form=Pet)]
async def post(self, _, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
"description": body.description
if body.description
else "",
}
)
_, response = app.test_client.post("/function", data=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
assert response.json["description"] == ""
_, response = app.test_client.post("/method", data=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
assert response.json["description"] == ""
def test_validate_query(app):
@dataclass
class Search:
q: str
@app.get("/function")
@validate(query=Search)
async def handler(_, query: Search):
return json({"q": query.q, "is_search": isinstance(query, Search)})
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(query=Search)]
async def get(self, _, query: Search):
return json({"q": query.q, "is_search": isinstance(query, Search)})
_, response = app.test_client.get("/function", params={"q": "Snoopy"})
assert response.status == 200
assert response.json["is_search"]
assert response.json["q"] == "Snoopy"
_, response = app.test_client.get("/method", params={"q": "Snoopy"})
assert response.status == 200
assert response.json["is_search"]
assert response.json["q"] == "Snoopy"
@@ -0,0 +1,394 @@
import sys
from typing import Optional
import pytest
from msgspec import Struct
from sanic import json
from sanic.views import HTTPMethodView
from sanic_ext import validate
from sanic_ext.extras.validation.check import check_data
from sanic_ext.extras.validation.schema import make_schema, parse_hint
from . import __models__ as models
SNOOPY_DATA = {"name": "Snoopy", "alter_ego": ["Flying Ace", "Joe Cool"]}
def test_schema():
class Pet(Struct):
name: str
class Person(Struct):
name: str
age: int
pets: Optional[list[Pet]]
schema = make_schema({}, Person)
assert "Person" in schema
assert schema["Person"]["hints"]["name"] == parse_hint(str)
assert schema["Person"]["hints"]["age"] == parse_hint(int)
assert schema["Person"]["hints"]["pets"] == parse_hint(Optional[list[Pet]])
assert "Pet" in schema
assert schema["Pet"]["hints"]["name"] == parse_hint(str)
def test_should_hydrate():
class Pet(Struct):
name: str
class Person(Struct):
name: str
age: int
pets: list[Pet]
data = {"name": "Charlie Brown", "age": 8, "pets": [{"name": "Snoopy"}]}
schema = make_schema({}, Person)
cb = check_data(Person, data, schema)
assert cb.name == "Charlie Brown"
assert cb.age == 8
assert cb.pets[0].name == "Snoopy"
@pytest.mark.parametrize(
"data",
(
{"name": "Charlie Brown", "age": 8, "pets": {"name": "Snoopy"}},
{"name": "Charlie Brown", "age": 8, "pets": [{"name": 123}]},
{"name": "Charlie Brown", "age": 8, "pets": [123]},
{"name": "Charlie Brown", "age": 8, "pets": 123},
{"name": "Charlie Brown", "age": "8", "pets": {"name": "Snoopy"}},
{"name": True, "age": 8, "pets": {"name": "Snoopy"}},
),
)
def test_should_not_hydrate(data):
class Pet(Struct):
name: str
class Person(Struct):
name: str
age: int
pets: list[Pet]
schema = make_schema({}, Person)
with pytest.raises(TypeError):
check_data(Person, data, schema)
@pytest.mark.parametrize(
"model,okay,data",
(
(models.ModelStr, True, {"foo": "bar"}),
(models.ModelStr, False, {"foo": 1}),
(models.ModelStr, False, {"foo": True}),
(models.ModelStr, False, {"foo": ["bar"]}),
(models.ModelStr, False, {"bar": "bar"}),
(models.ModelStr, False, {"foo": None}),
(models.ModelStr, False, 123),
(models.ModelInt, True, {"foo": 1}),
(models.ModelInt, True, {"foo": True}),
(models.ModelInt, False, {"foo": "1"}),
(models.ModelInt, False, {"foo": 1.1}),
(models.ModelInt, False, {"foo": None}),
(models.ModelFloat, True, {"foo": 1.1}),
(models.ModelFloat, False, {"foo": 1}),
(models.ModelFloat, False, {"foo": "1.1"}),
(models.ModelFloat, False, {"foo": None}),
(models.ModelBool, True, {"foo": True}),
(models.ModelBool, True, {"foo": False}),
(models.ModelBool, False, {"foo": 1}),
(models.ModelBool, False, {"foo": 0}),
(models.ModelBool, False, {"foo": 2}),
(models.ModelBool, False, {"foo": "True"}),
(models.ModelBool, False, {"foo": None}),
(models.ModelOptionalStr, True, {"foo": "bar"}),
(models.ModelOptionalStr, True, {"foo": None}),
(models.ModelOptionalStr, False, {"foo": 0}),
(models.ModelUnion, True, {"foo": 1}),
(models.ModelUnion, True, {"foo": 1.1}),
(models.ModelUnion, False, {"foo": "1.1"}),
(models.ModelUnion, False, {"foo": None}),
(models.ModelUnionModels, True, {"foo": {"foo": 1}}),
(models.ModelUnionModels, True, {"foo": {"foo": 1.1}}),
(models.ModelUnionModels, False, {"foo": {"foo": "1.1"}}),
(models.ModelUnionModels, False, {"foo": 1}),
(models.ModelUnionModels, False, {"foo": 1.1}),
(models.ModelUnionModels, False, {"foo": None}),
(models.ModelUnionStrInt, True, {"foo": "1"}),
(models.ModelUnionStrInt, True, {"foo": "1q"}),
(models.ModelUnionStrInt, True, {"foo": 1}),
(models.ModelUnionStrInt, False, {"foo": 1.1}),
(models.ModelUnionStrInt, False, {"foo": None}),
(models.ModelUnionIntStr, True, {"foo": "1"}),
(models.ModelUnionIntStr, True, {"foo": "1q"}),
(models.ModelUnionIntStr, True, {"foo": 1}),
(models.ModelUnionIntStr, False, {"foo": 1.1}),
(models.ModelUnionIntStr, False, {"foo": None}),
(models.ModelOptionalUnionStrInt, True, {"foo": "1"}),
(models.ModelOptionalUnionStrInt, True, {"foo": "1q"}),
(models.ModelOptionalUnionStrInt, True, {"foo": 1}),
(models.ModelOptionalUnionStrInt, False, {"foo": 1.1}),
(models.ModelOptionalUnionStrInt, True, {"foo": None}),
(models.ModelOptionalUnionIntStr, True, {"foo": "1"}),
(models.ModelOptionalUnionIntStr, True, {"foo": "1q"}),
(models.ModelOptionalUnionIntStr, True, {"foo": 1}),
(models.ModelOptionalUnionIntStr, False, {"foo": 1.1}),
(models.ModelOptionalUnionIntStr, True, {"foo": None}),
(models.ModelListStr, True, {"foo": ["bar"]}),
(models.ModelListStr, True, {"foo": ["one", "two"]}),
(models.ModelListStr, False, {"foo": "bar"}),
(models.ModelListStr, False, {"foo": ["one", 2]}),
(models.ModelListStr, False, {"foo": ["one", None]}),
(models.ModelListStr, False, {"foo": None}),
(models.ModelListModel, True, {"foo": [{"foo": "bar"}]}),
(
models.ModelListModel,
True,
{"foo": [{"foo": "one"}, {"foo": "two"}]},
),
(models.ModelListModel, False, {"foo": {"foo": "bar"}}),
(models.ModelListModel, False, {"foo": [{"foo": "bar"}, 2]}),
(models.ModelListModel, False, {"foo": [{"foo": "bar"}, None]}),
(models.ModelListModel, False, {"foo": None}),
(models.ModelOptionalList, True, {"foo": None}),
(models.ModelOptionalList, True, {"foo": ["bar"]}),
(models.ModelOptionalList, False, {"foo": [1]}),
(models.ModelOptionalList, False, {"foo": [None]}),
(models.ModelListUnion, True, {"foo": [1]}),
(models.ModelListUnion, True, {"foo": [1.1]}),
(models.ModelListUnion, True, {"foo": [1, 1.1]}),
(models.ModelListUnion, False, {"foo": [1, 1.1, "one"]}),
(models.ModelListUnion, False, {"foo": [1, 1.1, None]}),
(models.ModelListUnion, False, {"foo": 1}),
(models.ModelListUnion, False, {"foo": 1.1}),
(models.ModelListUnion, False, {"foo": None}),
(models.ModelOptionalListUnion, True, {"foo": [1]}),
(models.ModelOptionalListUnion, True, {"foo": [1.1]}),
(models.ModelOptionalListUnion, True, {"foo": [1, 1.1]}),
(models.ModelOptionalListUnion, True, {"foo": None}),
(models.ModelOptionalListUnion, False, {"foo": [1, 1.1, "one"]}),
(models.ModelOptionalListUnion, False, {"foo": [1, 1.1, None]}),
(models.ModelOptionalListUnion, False, {"foo": 1}),
(models.ModelOptionalListUnion, False, {"foo": 1.1}),
(models.ModelModel, True, {"foo": {"foo": "one"}}),
(models.ModelModel, False, {"foo": {"foo": 1}}),
(models.ModelModel, False, {"foo": {"foo": None}}),
(models.ModelModel, False, {"foo": "one"}),
(models.ModelModel, False, {"foo": None}),
(models.ModelOptionalModel, True, {"foo": {"foo": "one"}}),
(models.ModelOptionalModel, True, {"foo": None}),
(models.ModelOptionalModel, False, {"foo": {"foo": 1}}),
(models.ModelOptionalModel, False, {"foo": {"foo": None}}),
(models.ModelOptionalModel, False, {"foo": "one"}),
(models.ModelDictStr, True, {"foo": {"foo": "one"}}),
(models.ModelDictStr, False, {"foo": {"foo": 1}}),
(models.ModelDictStr, False, {"foo": {"foo": None}}),
(models.ModelDictStr, False, {"foo": "one"}),
(models.ModelDictStr, False, {"foo": None}),
(models.ModelDictModel, True, {"foo": {"foo": {"foo": "one"}}}),
(models.ModelDictModel, False, {"foo": {"foo": {"foo": 1}}}),
(models.ModelDictModel, False, {"foo": {"foo": 1}}),
(models.ModelDictModel, False, {"foo": {"foo": None}}),
(models.ModelDictModel, False, {"foo": "one"}),
(models.ModelDictModel, False, {"foo": None}),
(models.ModelOptionalDict, True, {"foo": {"foo": "one"}}),
(models.ModelOptionalDict, True, {"foo": None}),
(models.ModelOptionalDict, False, {"foo": {"foo": 1}}),
(models.ModelOptionalDict, False, {"foo": {"foo": None}}),
(models.ModelOptionalDict, False, {"foo": "one"}),
(models.ModelDictUnion, True, {"foo": {"foo": 1}}),
(models.ModelDictUnion, True, {"foo": {"foo": 1.1}}),
(models.ModelDictUnion, False, {"foo": {"foo": "one"}}),
(models.ModelDictUnion, False, {"foo": {"foo": None}}),
(models.ModelDictUnion, False, {"foo": "one"}),
(models.ModelDictUnion, False, {"foo": 1}),
(models.ModelDictUnion, False, {"foo": 1.1}),
(models.ModelDictUnion, False, {"foo": None}),
(models.ModelOptionalDictUnion, True, {"foo": {"foo": 1}}),
(models.ModelOptionalDictUnion, True, {"foo": {"foo": 1.1}}),
(models.ModelOptionalDictUnion, True, {"foo": None}),
(models.ModelOptionalDictUnion, False, {"foo": {"foo": "one"}}),
(models.ModelOptionalDictUnion, False, {"foo": {"foo": None}}),
(models.ModelOptionalDictUnion, False, {"foo": "one"}),
(models.ModelOptionalDictUnion, False, {"foo": 1}),
(models.ModelOptionalDictUnion, False, {"foo": 1.1}),
(models.ModelSingleLiteral, True, {"foo": True}),
(models.ModelSingleLiteral, False, {"foo": False}),
(models.ModelSingleLiteral, False, {"foo": "True"}),
(models.ModelSingleLiteral, False, {"foo": None}),
(models.ModelOptionalSingleLiteral, True, {"foo": True}),
(models.ModelOptionalSingleLiteral, True, {"foo": None}),
(models.ModelOptionalSingleLiteral, False, {"foo": False}),
(models.ModelOptionalSingleLiteral, False, {"foo": "True"}),
(models.ModelOptionalMultipleLiteral, True, {"foo": True}),
(models.ModelOptionalMultipleLiteral, True, {"foo": 1}),
(models.ModelOptionalMultipleLiteral, True, {"foo": "y"}),
(models.ModelOptionalMultipleLiteral, True, {"foo": "Y"}),
(models.ModelOptionalMultipleLiteral, True, {"foo": None}),
(models.ModelOptionalMultipleLiteral, False, {"foo": "n"}),
(models.ModelOptionalMultipleLiteral, False, {"foo": False}),
(models.ModelListStrWithDefaultFactory, True, {}),
(models.ModelListStrWithDefaultFactory, True, {"foo": ["bar"]}),
(models.ModelListStrWithDefaultFactory, True, {"foo": []}),
(models.ModelListStrWithDefaultFactory, False, {"foo": [1]}),
(models.ModelListStrWithDefaultFactory, False, {"foo": None}),
),
)
def test_modeling(model, okay, data):
schema = make_schema({}, model)
if okay:
check_data(model, data, schema)
else:
with pytest.raises(TypeError):
check_data(model, data, schema)
@pytest.mark.skipif(
sys.version_info < (3, 10), reason="UnionType added in 3.10"
)
def test_modeling_union_type_ModelUnionTypeStrNone():
schema = make_schema({}, models.ModelUnionTypeStrNone)
check_data(models.ModelUnionTypeStrNone, {"foo": "bar"}, schema)
check_data(models.ModelUnionTypeStrNone, {"foo": None}, schema)
with pytest.raises(TypeError):
check_data(models.ModelUnionTypeStrNone, {"foo": 0}, schema)
@pytest.mark.skipif(
sys.version_info < (3, 10), reason="UnionType added in 3.10"
)
def test_modeling_union_type_ModelUnionTypeStrIntNone():
schema = make_schema({}, models.ModelUnionTypeStrIntNone)
check_data(models.ModelUnionTypeStrIntNone, {"foo": "1"}, schema)
check_data(models.ModelUnionTypeStrIntNone, {"foo": "bar"}, schema)
check_data(models.ModelUnionTypeStrIntNone, {"foo": None}, schema)
check_data(models.ModelUnionTypeStrIntNone, {"foo": 1}, schema)
check_data(models.ModelUnionTypeStrIntNone, {"foo": 0}, schema)
with pytest.raises(TypeError):
check_data(models.ModelUnionTypeStrIntNone, {"foo": 1.1}, schema)
@pytest.mark.skipif(
sys.version_info < (3, 10), reason="UnionType added in 3.10"
)
def test_modeling_union_type_ModelUnionTypeStrInt():
schema = make_schema({}, models.ModelUnionTypeStrInt)
check_data(models.ModelUnionTypeStrInt, {"foo": "1"}, schema)
check_data(models.ModelUnionTypeStrInt, {"foo": "bar"}, schema)
check_data(models.ModelUnionTypeStrInt, {"foo": 1}, schema)
check_data(models.ModelUnionTypeStrInt, {"foo": 0}, schema)
with pytest.raises(TypeError):
check_data(models.ModelUnionTypeStrInt, {"foo": None}, schema)
with pytest.raises(TypeError):
check_data(models.ModelUnionTypeStrInt, {"foo": 1.1}, schema)
def test_validate_json(app):
class Pet(Struct):
name: str
alter_ego: list[str]
@app.post("/function")
@validate(json=Pet)
async def handler(_, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(json=Pet)]
async def post(self, _, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
_, response = app.test_client.post("/function", json=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
_, response = app.test_client.post("/method", json=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
def test_validate_form(app):
class Pet(Struct):
name: str
alter_ego: list[str]
@app.post("/function")
@validate(form=Pet)
async def handler(_, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(form=Pet)]
async def post(self, _, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
_, response = app.test_client.post("/function", data=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
_, response = app.test_client.post("/method", data=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
def test_validate_query(app):
class Search(Struct):
q: str
@app.get("/function")
@validate(query=Search)
async def handler(_, query: Search):
return json({"q": query.q, "is_search": isinstance(query, Search)})
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(query=Search)]
async def get(self, _, query: Search):
return json({"q": query.q, "is_search": isinstance(query, Search)})
_, response = app.test_client.get("/function", params={"q": "Snoopy"})
assert response.status == 200
assert response.json["is_search"]
assert response.json["q"] == "Snoopy"
_, response = app.test_client.get("/method", params={"q": "Snoopy"})
assert response.status == 200
assert response.json["is_search"]
assert response.json["q"] == "Snoopy"
@@ -0,0 +1,42 @@
from dataclasses import asdict, dataclass
from sanic import Sanic, json
from sanic_ext.extras.validation.decorator import validate
@dataclass
class ModelA:
a: str
@dataclass
class ModelB:
b: str
def test_both_body_and_query(app: Sanic):
@app.post("")
@validate(json=ModelA, query=ModelB)
async def test(_, body: ModelA, query: ModelB):
return json(
{
"body": asdict(body),
"query": asdict(query),
}
)
_, response = app.test_client.post("")
assert response.status == 400
_, response = app.test_client.post("", params={"b": "bbb"})
assert response.status == 400
_, response = app.test_client.post(
"", params={"b": "bbb"}, json={"a": "aaa"}
)
assert response.status == 200
assert response.json == {
"body": {"a": "aaa"},
"query": {"b": "bbb"},
}
@@ -0,0 +1,148 @@
import pydantic
from pydantic.dataclasses import dataclass
from sanic import json
from sanic.views import HTTPMethodView
from sanic_ext import validate
from sanic_ext.exceptions import ValidationError
SNOOPY_DATA = {"name": "Snoopy", "alter_ego": ["Flying Ace", "Joe Cool"]}
def test_validate_json(app):
@dataclass
class Pet:
name: str
alter_ego: list[str]
@app.post("/function")
@validate(json=Pet)
async def handler(_, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(json=Pet)]
async def post(self, _, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
_, response = app.test_client.post("/function", json=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
_, response = app.test_client.post("/method", json=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
def test_validate_form(app):
@dataclass
class Pet:
name: str
alter_ego: list[str]
@app.post("/function")
@validate(form=Pet)
async def handler(_, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(form=Pet)]
async def post(self, _, body: Pet):
return json(
{
"is_pet": isinstance(body, Pet),
"pet": {"name": body.name, "alter_ego": body.alter_ego},
}
)
_, response = app.test_client.post("/function", data=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
_, response = app.test_client.post("/method", data=SNOOPY_DATA)
assert response.status == 200
assert response.json["is_pet"]
assert response.json["pet"] == SNOOPY_DATA
def test_validate_query(app):
@dataclass
class Search:
q: str
@app.get("/function")
@validate(query=Search)
async def handler(_, query: Search):
return json({"q": query.q, "is_search": isinstance(query, Search)})
class MethodView(HTTPMethodView, attach=app, uri="/method"):
decorators = [validate(query=Search)]
async def get(self, _, query: Search):
return json({"q": query.q, "is_search": isinstance(query, Search)})
_, response = app.test_client.get("/function", params={"q": "Snoopy"})
assert response.status == 200
assert response.json["is_search"]
assert response.json["q"] == "Snoopy"
_, response = app.test_client.get("/method", params={"q": "Snoopy"})
assert response.status == 200
assert response.json["is_search"]
assert response.json["q"] == "Snoopy"
class User(pydantic.BaseModel):
name: str
age: int
email: str
def test_success_validate_form_custom_message(app):
@app.post("/user")
@validate(form=User)
async def create_user(request, body: User):
return json(body.dict())
user = {"name": "Alison", "age": 25, "email": "alison@almeida.com"}
_, response = app.test_client.post("/user", data=user)
assert response.status == 200
def test_error_validate_form_custom_message(app):
async def server_error_validate_form(request, exception: ValidationError):
error = exception.extra["exception"]
return json(error, status=400)
@app.post("/user")
@validate(form=User)
async def create_user(request, body: User):
return json(body.dict())
user = {"name": "Alison", "age": 25}
app.error_handler.add(ValidationError, server_error_validate_form)
_, response = app.test_client.post("/user", data=user)
assert response.status == 400