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:
@@ -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
|
||||
Reference in New Issue
Block a user