Compare commits

..

10 Commits

Author SHA1 Message Date
kennethreitz eed5365fe0 file upload support 2018-10-20 09:56:35 -04:00
kennethreitz f5905568c4 files support 2018-10-20 09:54:53 -04:00
kennethreitz 096099470e yay tests pass 2018-10-20 08:50:36 -04:00
kennethreitz e7ed7aca3c tests still pass 2018-10-20 08:23:10 -04:00
kennethreitz 6725b275b8 cleanup 2018-10-20 07:59:39 -04:00
kennethreitz 3447a7ef41 v0.1.5 2018-10-20 07:59:12 -04:00
kennethreitz 99f35fbea4 use querydict for form parsing 2018-10-20 07:57:27 -04:00
kennethreitz 5c9a3912a9 cached _content 2018-10-20 07:38:53 -04:00
kennethreitz 5bb9f96701 cleanup 2018-10-19 05:11:07 -07:00
kennethreitz 750e9dfaa7 cleanup 2018-10-19 04:54:49 -07:00
9 changed files with 156 additions and 55 deletions
+4
View File
@@ -1,3 +1,7 @@
# v0.1.5
- Improvements to sequential media reading.
- File upload support.
# v0.1.4
- Stability.
Generated
+9 -2
View File
@@ -181,6 +181,13 @@
],
"version": "==2.20.0"
},
"requests-toolbelt": {
"hashes": [
"sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237",
"sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5"
],
"version": "==0.8.0"
},
"responder": {
"editable": true,
"path": "."
@@ -221,9 +228,9 @@
},
"uvicorn": {
"hashes": [
"sha256:30096b58325cdca8e547a6f5f4300040d0b8763f573cb1843abfa96f81a49cf8"
"sha256:7c4550c7e6f7c8727fa5ccd5200baf62c9e055895e058933ee88f5d0c246ca0c"
],
"version": "==0.3.13"
"version": "==0.3.14"
},
"websockets": {
"hashes": [
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.4"
__version__ = "0.1.5"
+59 -44
View File
@@ -64,7 +64,7 @@ class API:
)
self.routes = {}
self.schemas = {}
self.session_cookie = 'Responder-Session'
self.session_cookie = "Responder-Session"
self.hsts_enabled = enable_hsts
self.static_files = StaticFiles(directory=str(self.static_dir))
@@ -95,13 +95,9 @@ class API:
[str(self.templates_dir), str(self.built_in_templates_dir)],
followlinks=True,
),
autoescape=jinja2.select_autoescape(
["html", "xml"] if auto_escape else []
)
autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []),
)
self.jinja_values_base = {
"api": self, # Give reference to self.
}
self.jinja_values_base = {"api": self} # Give reference to self.
@property
def _apispec(self):
@@ -209,49 +205,59 @@ class API:
if resp.session:
data = self._signer.sign(json.dumps(resp.session).encode("utf-8"))
resp.cookies[self.session_cookie] = data.decode("utf-8")
@staticmethod
def no_response(req, resp, **params):
pass
async def _dispatch_request(self, req):
# Set formats on Request object.
req.formats = self.formats
# Get the route.
route = self.path_matches_route(req.url.path)
route = self.routes.get(route)
params = route.incoming_matches(req.url.path)
# Create the response object.
resp = models.Response(req=req, formats=self.formats)
cont = False
if route:
try:
params = self.routes[route].incoming_matches(req.url.path)
result = self.routes[route].endpoint(req, resp, **params)
if hasattr(result, "cr_running"):
await result
# The request is using class-based views.
except TypeError as e:
try:
view = self.routes[route].endpoint(**params)
except TypeError:
view = self.routes[route].endpoint
if route.is_graphql:
await self.graphql_response(req, resp, schema=route.endpoint)
if self.routes[route].is_graphql:
await self.graphql_response(req, resp, schema=view)
else:
pass
elif route.is_function:
try:
# Run the view.
r = route.endpoint(req, resp, **params)
# If it's async, await it.
if hasattr(r, "cr_running"):
await r
except TypeError as e:
cont = True
if route.is_class_based or cont:
try:
view = route.endpoint(**params)
except TypeError:
view = route.endpoint
# Run on_request first.
try:
r = getattr(view, "on_request")(req, resp, **params)
if hasattr(r, "send"):
await r
except AttributeError:
pass
# Run the view.
r = getattr(view, "on_request", self.no_response)(req, resp, **params)
# If it's async, await it.
if hasattr(r, "send"):
await r
# Then on_get.
method = req.method
try:
r = getattr(view, f"on_{method}")(req, resp, **params)
if hasattr(r, "send"):
await r
except AttributeError:
pass
# Run the view.
r = getattr(view, f"on_{method}", self.no_response)(req, resp, **params)
# If it's async, await it.
if hasattr(r, "send"):
await r
else:
self.default_response(req, resp)
@@ -280,6 +286,13 @@ class API:
if default:
self.default_endpoint = endpoint
try:
if callable(endpoint):
endpoint.is_routed = True
except AttributeError:
pass
self.routes[route] = Route(route, endpoint)
# TODO: A better datastructer or sort it once the app is loaded
self.routes = dict(
@@ -328,7 +341,11 @@ class API:
if "json" in req.mimetype:
json_media = await req.media("json")
return json_media["query"], json_media.get("variables"), json_media.get("operationName")
return (
json_media["query"],
json_media.get("variables"),
json_media.get("operationName"),
)
# Support query/q in form data.
# Form data is awaiting https://github.com/encode/starlette/pull/102
@@ -355,7 +372,9 @@ class API:
return
query, variables, operation_name = await self._resolve_graphql_query(req)
result = schema.execute(query, variables=variables, operation_name=operation_name)
result = schema.execute(
query, variables=variables, operation_name=operation_name
)
result, status_code = encode_execution_results(
[result],
is_batch=False,
@@ -410,6 +429,8 @@ class API:
for (route, route_object) in self.routes.items():
if route_object.endpoint == endpoint:
return route_object.url(testing=testing, **params)
elif route_object.endpoint_name == endpoint:
return route_object.url(testing=testing, **params)
raise ValueError
def static_url(self, asset):
@@ -425,10 +446,7 @@ class API:
:param values: Data to pass into the template.
"""
# Prepopulate values with base
values = {
**self.jinja_values_base,
**values,
}
values = {**self.jinja_values_base, **values}
template = self.jinja_env.get_template(name_)
return template.render(**values)
@@ -442,10 +460,7 @@ class API:
:param values: Data to pass into the template.
"""
# Prepopulate values with base
values = {
**self.jinja_values_base,
**values,
}
values = {**self.jinja_values_base, **values}
template = self.jinja_env.from_string(s_)
return template.render(**values)
+42 -3
View File
@@ -1,10 +1,17 @@
from urllib.parse import parse_qs
import yaml
import json
from parse import findall
from .models import QueryDict
from requests_toolbelt.multipart import decoder
async def format_form(r, encode=False):
if not encode:
return await r._starlette.form()
if encode:
pass
else:
return QueryDict(await r.text)
async def format_yaml(r, encode=False):
@@ -23,5 +30,37 @@ async def format_json(r, encode=False):
return json.loads(await r.content)
async def format_files(r, encode=False):
if encode:
pass
else:
decoded = decoder.MultipartDecoder(await r.content, r.headers["Content-Type"])
dump = {}
for part in decoded.parts:
header = part.headers[b"Content-Disposition"].decode("utf-8")
filename = None
for section in [h.strip() for h in header.split(";")]:
split = section.split("=")
if len(split) > 1:
key = split[0]
value = split[1]
value = value[1:-1]
if key == "filename":
filename = value
content = part.text
if filename:
dump[filename] = content
return dump
def get_formats():
return {"json": format_json, "yaml": format_yaml, "form": format_form}
return {
"json": format_json,
"yaml": format_yaml,
"form": format_form,
"files": format_files,
}
+6 -3
View File
@@ -90,13 +90,14 @@ class QueryDict(dict):
# TODO: add slots
class Request:
__slots__ = ["_starlette", "formats", "_headers", "_encoding", "api"]
__slots__ = ["_starlette", "formats", "_headers", "_encoding", "api", "_content"]
def __init__(self, scope, receive, api=None):
self._starlette = StarletteRequest(scope, receive)
self.formats = None
self._encoding = None
self.api = api
self._content = None
headers = CaseInsensitiveDict()
for header, value in self._starlette.headers.items():
@@ -179,12 +180,14 @@ class Request:
@property
async def content(self):
"""The Request body, as bytes. Must be awaited."""
return await self._starlette.body()
if not self._content:
self._content = await self._starlette.body()
return self._content
@property
async def text(self):
"""The Request body, as unicode. Must be awaited."""
return (await self._starlette.body()).decode(await self.encoding)
return (await self.content).decode(await self.encoding)
@property
async def declared_encoding(self):
+15
View File
@@ -31,6 +31,11 @@ class Route:
# Strings.
return self.does_match(other)
@property
def endpoint_name(self):
print(self.endpoint.__name__)
return self.endpoint.__name__
@property
def description(self):
return self.endpoint.__doc__
@@ -67,3 +72,13 @@ class Route:
@property
def is_graphql(self):
return hasattr(self.endpoint, "get_graphql_type")
@property
def is_class_based(self):
return hasattr(self.endpoint, "__class__")
def is_function(self):
routed = hasattr(self.endpoint, "is_routed")
code = hasattr(self.endpoint, "__code__")
kwdefaults = hasattr(self.endpoint, "__kwdefaults__")
return all((routed, code, kwdefaults))
+1
View File
@@ -40,6 +40,7 @@ required = [
"asgiref",
"docopt",
"itsdangerous",
"requests-toolbelt",
]
+19 -2
View File
@@ -1,6 +1,7 @@
import pytest
import yaml
import responder
import io
def test_api_basic_route(api):
@@ -395,13 +396,29 @@ def test_sessions(api, session):
assert "Responder-Session" in r.cookies
r = session.get(api.url_for(view))
assert r.cookies['Responder-Session'] == '{"hello": "world"}.lJVWJULPqR9kdao_oT4pUglV281bxHfGvcKQ7XF8qNqaiIZlRcMvqKNdA1-d5z7DycAx5eqmzJZoqWPP759-Cw'
assert (
r.cookies["Responder-Session"]
== '{"hello": "world"}.lJVWJULPqR9kdao_oT4pUglV281bxHfGvcKQ7XF8qNqaiIZlRcMvqKNdA1-d5z7DycAx5eqmzJZoqWPP759-Cw'
)
assert r.json() == {"hello": "world"}
def test_template_rendering(api, session):
@api.route('/')
@api.route("/")
def view(req, resp):
resp.content = api.template_string("{{ var }}", var="hello")
r = session.get(api.url_for(view))
assert r.text == "hello"
def test_file_uploads(api, session):
@api.route("/")
async def upload(req, resp):
resp.media = {"files": await req.media("files")}
world = io.StringIO("world")
data = {"hello": world}
r = session.get(api.url_for(upload), files=data)
assert r.json() == {"files": {"hello": "world"}}