Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for form data request #225

Merged
merged 4 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added examples/__init__.py
Empty file.
20 changes: 20 additions & 0 deletions examples/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from pydantic import BaseModel, Field

from spectree import BaseFile


class File(BaseModel):
uid: str
file: BaseFile


class FileResp(BaseModel):
filename: str
type: str


class Query(BaseModel):
text: str = Field(
...,
max_length=100,
)
119 changes: 119 additions & 0 deletions examples/falcon_asgi_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import logging
from random import random

import falcon.asgi
import uvicorn
from pydantic import BaseModel, Field

from examples.common import File, FileResp, Query
from spectree import Response, SpecTree, Tag

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger()

api = SpecTree(
"falcon-asgi",
title="Demo Service",
version="0.1.2",
unknown="test",
)

demo = Tag(name="demo", description="😊", externalDocs={"url": "https://github.com"})


class Resp(BaseModel):
label: int = Field(
...,
ge=0,
le=9,
)
score: float = Field(
...,
gt=0,
lt=1,
)


class BadLuck(BaseModel):
loc: str
msg: str
typ: str


class Data(BaseModel):
uid: str
limit: int
vip: bool


class Ping:
def check(self):
pass

@api.validate(tags=[demo])
async def on_get(self, req, resp):
"""
health check
"""
self.check()
logger.debug("ping <> pong")
resp.media = {"msg": "pong"}


class Classification:
"""
classification demo
"""

@api.validate(tags=[demo])
async def on_get(self, req, resp, source, target):
"""
API summary

description here: test information with `source` and `target`
"""
resp.media = {"msg": f"hello from {source} to {target}"}

@api.validate(
query=Query, json=Data, resp=Response(HTTP_200=Resp, HTTP_403=BadLuck)
)
async def on_post(self, req, resp, source, target):
"""
post demo

demo for `query`, `data`, `resp`, `x`
"""
logger.debug(f"{source} => {target}")
logger.info(req.context.query)
logger.info(req.context.json)
if random() < 0.5:
resp.status = falcon.HTTP_403
resp.media = {"loc": "unknown", "msg": "bad luck", "typ": "random"}
return
resp.media = {"label": int(10 * random()), "score": random()}


class FileUpload:
"""
file-handling demo
"""

@api.validate(form=File, resp=Response(HTTP_200=FileResp), tags=["file-upload"])
async def on_post(self, req, resp):
"""
post multipart/form-data demo

demo for 'form'
"""
file = req.context.form.file
resp.media = {"filename": file.filename, "type": file.type}


if __name__ == "__main__":
app = falcon.asgi.App()
app.add_route("/ping", Ping())
app.add_route("/api/{source}/{target}", Classification())
app.add_route("/api/file_upload", FileUpload())
api.register(app)

uvicorn.run(app, log_level="info")
27 changes: 18 additions & 9 deletions examples/falcon_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
import falcon
from pydantic import BaseModel, Field

from examples.common import File, FileResp, Query
from spectree import Response, SpecTree, Tag

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger()


api = SpecTree(
"falcon",
title="Demo Service",
Expand All @@ -24,13 +24,6 @@
demo = Tag(name="demo", description="😊", externalDocs={"url": "https://github.com"})


class Query(BaseModel):
text: str = Field(
...,
max_length=100,
)


class Resp(BaseModel):
label: int = Field(
...,
Expand Down Expand Up @@ -101,7 +94,22 @@ def on_post(self, req, resp, source, target):
resp.media = {"loc": "unknown", "msg": "bad luck", "typ": "random"}
return
resp.media = {"label": int(10 * random()), "score": random()}
# resp.media = Resp(label=int(10 * random()), score=random())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's another way to return the response (#212). I guess we can make it more clear in the comments.



class FileUpload:
"""
file-handling demo
"""

@api.validate(form=File, resp=Response(HTTP_200=FileResp), tags=["file-upload"])
def on_post(self, req, resp):
"""
post multipart/form-data demo

demo for 'form'
"""
file = req.context.form.file
resp.media = {"filename": file.filename, "type": file.type}


if __name__ == "__main__":
Expand All @@ -113,6 +121,7 @@ def on_post(self, req, resp, source, target):
app = falcon.App()
app.add_route("/ping", Ping())
app.add_route("/api/{source}/{target}", Classification())
app.add_route("/api/file_upload", FileUpload())
api.register(app)

httpd = simple_server.make_server("localhost", 8000, app)
Expand Down
18 changes: 13 additions & 5 deletions examples/flask_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,13 @@
from flask.views import MethodView
from pydantic import BaseModel, Field

from examples.common import File, FileResp, Query
from spectree import Response, SpecTree

app = Flask(__name__)
api = SpecTree("flask")


class Query(BaseModel):
text: str = "default query strings"


class Resp(BaseModel):
label: int
score: float = Field(
Expand Down Expand Up @@ -89,11 +86,22 @@ def with_code_header():
return jsonify(language=request.context.headers.Lang), 203, {"X": 233}


@app.route("/api/file_upload", methods=["POST"])
@api.validate(form=File, resp=Response(HTTP_200=FileResp), tags=["file-upload"])
def with_file():
"""
post multipart/form-data demo

demo for 'form'
"""
file = request.context.form.file
return {"filename": file.filename, "type": file.content_type}


class UserAPI(MethodView):
@api.validate(json=Data, resp=Response(HTTP_200=Resp), tags=["test"])
def post(self):
return jsonify(label=int(10 * random()), score=random())
# return Resp(label=int(10 * random()), score=random())


if __name__ == "__main__":
Expand Down
24 changes: 17 additions & 7 deletions examples/starlette_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@
from starlette.responses import JSONResponse
from starlette.routing import Mount, Route

from examples.common import File, FileResp, Query
from spectree import Response, SpecTree

# from spectree.plugins.starlette_plugin import PydanticResponse

api = SpecTree("starlette")


class Query(BaseModel):
text: str


class Resp(BaseModel):
label: int = Field(
...,
Expand Down Expand Up @@ -48,6 +43,17 @@ async def predict(request):
# return PydanticResponse(Resp(label=5, score=0.5))


@api.validate(form=File, resp=Response(HTTP_200=FileResp), tags=["file-upload"])
async def file_upload(request):
"""
post multipart/form-data demo

demo for 'form'
"""
file = request.context.form.file
return JSONResponse({"filename": file.filename, "type": file.type})


class Ping(HTTPEndpoint):
@api.validate(tags=["health check", "api"])
def get(self, request):
Expand All @@ -67,7 +73,11 @@ def get(self, request):
routes=[
Route("/ping", Ping),
Mount(
"/api", routes=[Route("/predict/{luck:int}", predict, methods=["POST"])]
"/api",
routes=[
Route("/predict/{luck:int}", predict, methods=["POST"]),
Route("/file-upload", file_upload, methods=["POST"]),
],
),
]
)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"black~=22.3",
"isort~=5.10",
"autoflake~=1.4",
"mypy>=0.942",
"mypy>=0.971",
],
},
zip_safe=False,
Expand Down
4 changes: 2 additions & 2 deletions spectree/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import logging

from .models import SecurityScheme, Tag
from .models import BaseFile, SecurityScheme, Tag
from .response import Response
from .spec import SpecTree

__all__ = ["SpecTree", "Response", "Tag", "SecurityScheme"]
__all__ = ["SpecTree", "Response", "Tag", "SecurityScheme", "BaseFile"]

# setup library logging
logging.getLogger(__name__).addHandler(logging.NullHandler())
22 changes: 22 additions & 0 deletions spectree/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,25 @@ class Server(BaseModel):

class Config:
validate_assignment = True


class BaseFile:
"""
An uploaded file included as part of the request data.
"""

@classmethod
def __get_validators__(cls):
# one or more validators may be yielded which will be called in the
# order to validate the input, each validator will receive as an input
# the value returned from the previous validator
yield cls.validate

@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(format="binary", type="string")

@classmethod
def validate(cls, value: Any):
# https://github.com/luolingchun/flask-openapi3/blob/master/flask_openapi3/models/file.py
return value
22 changes: 19 additions & 3 deletions spectree/plugins/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import logging
from collections import namedtuple
from typing import TYPE_CHECKING, Any, Callable, Generic, Mapping, Optional, TypeVar
from typing import (
TYPE_CHECKING,
Any,
Callable,
Generic,
Mapping,
NamedTuple,
Optional,
TypeVar,
)

from .._types import ModelType
from ..config import Configuration
Expand All @@ -10,7 +18,13 @@
# to avoid cyclic import
from ..spec import SpecTree

Context = namedtuple("Context", ["query", "json", "headers", "cookies"])

class Context(NamedTuple):
query: list
json: list
form: list
headers: dict
cookies: dict


BackendRoute = TypeVar("BackendRoute")
Expand All @@ -25,6 +39,7 @@ class BasePlugin(Generic[BackendRoute]):

# ASYNC: is it an async framework or not
ASYNC = False
FORM_MIMETYPE = ("application/x-www-form-urlencoded", "multipart/form-data")

def __init__(self, spectree: "SpecTree"):
self.spectree = spectree
Expand All @@ -44,6 +59,7 @@ def validate(
func: Callable,
query: Optional[ModelType],
json: Optional[ModelType],
form: Optional[ModelType],
headers: Optional[ModelType],
cookies: Optional[ModelType],
resp: Optional[Response],
Expand Down
Loading