Compare commits

...

25 Commits

Author SHA1 Message Date
kennethreitz 19f8553f2d fix 2018-10-18 04:31:38 -07:00
kennethreitz 05a64ff095 cookies 2018-10-18 04:31:22 -07:00
kennethreitz a8fc78fcda fixes #76 2018-10-18 04:26:25 -07:00
kennethreitz e0e8b40fa2 Merge pull request #91 from kennethreitz/cookies
Cookies
2018-10-18 04:23:16 -07:00
kennethreitz 00165cd6ca tests for cookies 2018-10-18 04:16:44 -07:00
kennethreitz cd799ddfcd cookies 2018-10-18 04:07:13 -07:00
kennethreitz fffd6b7c86 Merge pull request #83 from kennethreitz/bnm_tests
added more tests to routes, and changed some bits in routes regarding regex
2018-10-18 02:51:55 -07:00
kennethreitz 439b008a34 Merge pull request #85 from condemil/patch-1
Add .python-version to .gitignore
2018-10-18 02:51:42 -07:00
kennethreitz f38e538892 Merge pull request #89 from pyasi/tests_for_status_codes
Add basic tests for the status codes file
2018-10-18 02:50:57 -07:00
Peter Yasi 6aa87a073f Add basic tests for the status codes file 2018-10-17 21:25:28 -04:00
Dmitry c38198ccba Add .python-version to .gitignore
.python-version allows to specify separate pyenv virtual environment
2018-10-17 22:58:16 +02:00
Luna 3be88c8cbf removed redundant import in routes.py 2018-10-17 21:17:14 +01:00
Luna 558ced1afb recommented pytest.ini addopts 2018-10-17 21:07:39 +01:00
Luna 0149e6935d added more tests to routes, and changed some bits in routes regarding regex 2018-10-17 21:05:38 +01:00
kennethreitz d97fdfd7c4 Merge pull request #75 from tomchristie/asgi-middleware
Support ASGI middleware
2018-10-17 12:03:15 -07:00
kennethreitz 8b85d8c6fb Merge pull request #80 from taoufik07/fix-CBV-missing-prams
Fix CBV missing params
2018-10-17 12:02:23 -07:00
kennethreitz 673779490c Merge pull request #82 from squiddy/patch-1
Fix docker image typo in deployment documentation
2018-10-17 12:01:43 -07:00
Reiner Gerecke 48154e7e2d Fix docker image typo in deployment documentation 2018-10-17 19:59:40 +02:00
taoufik07 20f72b3f63 Add tests 2018-10-17 18:43:24 +01:00
taoufik07 e82c958af2 Add missing params to on_method 2018-10-17 18:20:44 +01:00
taoufik07 60c311ab9f Add missing params to on_request 2018-10-17 18:20:16 +01:00
Tom Christie fbac81c245 Drop commented out gzip code 2018-10-17 15:13:09 +01:00
Tom Christie 9ca67d9228 Support ASGI middleware 2018-10-17 15:11:16 +01:00
kennethreitz 5ffa18221f an 2018-10-17 06:20:06 -07:00
kennethreitz aceb1f0f61 Must be awaited. 2018-10-17 06:17:21 -07:00
12 changed files with 197 additions and 75 deletions
+1
View File
@@ -1,6 +1,7 @@
.vscode/
.cache
.idea
.python-version
.coverage
.pytest_cache
.DS_Store
+3
View File
@@ -1,3 +1,6 @@
# v0.1.2
- Cookies support.
# v0.1.1
- Default routes.
+1
View File
@@ -14,6 +14,7 @@ twine = "*"
flask = "*"
sphinx = "*"
marshmallow = "*"
pytest-cov = "*"
[requires]
python_version = "3.7"
+2 -2
View File
@@ -10,7 +10,7 @@ Assuming existing ``api.py`` and ``Pipfile.lock`` containing ``responder``.
``Dockerfile``::
from kenethreitz/pipenv
from kennethreitz/pipenv
COPY . /app
CMD python3 api.py
@@ -33,7 +33,7 @@ Install Responder::
$ pipenv install responder
...
Write out a ``api.py``::
Write out an ``api.py``::
import responder
+1 -1
View File
@@ -1,4 +1,4 @@
[pytest]
; addopts= -rsxX -s -v --strict
;addopts= -rsxX -s -v --strict
filterwarnings =
error::UserWarning
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.1"
__version__ = "0.1.2"
+24 -6
View File
@@ -11,6 +11,7 @@ from graphql_server import encode_execution_results, json_encode, default_format
from starlette.routing import Router
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from starlette.middleware.gzip import GZipMiddleware
from apispec import APISpec
from apispec.ext.marshmallow import MarshmallowPlugin
from apispec import yaml_utils
@@ -76,6 +77,8 @@ class API:
self.add_route(openapi_route, self.schema_response)
self.default_endpoint = None
self.app = self.dispatch
self.add_middleware(GZipMiddleware)
@property
def _apispec(self):
@@ -102,6 +105,9 @@ class API:
def openapi(self):
return self._apispec.to_yaml()
def add_middleware(self, middleware_cls, **middleware_config):
self.app = middleware_cls(self.app, **middleware_config)
def __call__(self, scope):
path = scope["path"]
root_path = scope.get("root_path", "")
@@ -117,6 +123,9 @@ class API:
app = WsgiToAsgi(app)
return app(scope)
return self.app(scope)
def dispatch(self, scope):
# Call the main dispatcher.
async def asgi(receive, send):
nonlocal scope, self
@@ -162,6 +171,11 @@ class API:
if route_object.does_match(path):
return route
def _prepare_cookies(self, resp):
if resp.cookies:
header = " ".join([f"{k}={v}" for k, v in resp.cookies.items()])
resp.headers["Set-Cookie"] = header
async def _dispatch_request(self, req):
# Set formats on Request object.
req.formats = self.formats
@@ -201,7 +215,7 @@ class API:
# Run on_request first.
try:
r = getattr(view, "on_request")(req, resp)
r = getattr(view, "on_request")(req, resp, **params)
if hasattr(r, "send"):
await r
except AttributeError:
@@ -211,7 +225,7 @@ class API:
method = req.method
try:
r = getattr(view, f"on_{method}")(req, resp)
r = getattr(view, f"on_{method}")(req, resp, **params)
if hasattr(r, "send"):
await r
except AttributeError:
@@ -219,9 +233,13 @@ class API:
else:
self.default_response(req, resp)
self._prepare_cookies(resp)
return resp
def add_route(self, route, endpoint=None, *, default=False, static=False, check_existing=True):
def add_route(
self, route, endpoint=None, *, default=False, static=False, check_existing=True
):
"""Add a route to the API.
:param route: A string representation of the route.
@@ -372,12 +390,12 @@ class API:
"""Given a static asset, return its URL path."""
return f"{self.static_route}/{str(asset)}"
def template(self, name, auto_escape=True, **values):
def template(self, name_, auto_escape=True, **values):
"""Renders the given `jinja2 <http://jinja.pocoo.org/docs/>`_ template, with provided values supplied.
Note: The current ``api`` instance is always passed into the view.
:param name: The filename of the jinja2 template, in ``templates_dir``.
:param name_: The filename of the jinja2 template, in ``templates_dir``.
:param auto_escape: If ``True``, HTML and XML will automatically be escaped.
:param values: Data to pass into the template.
"""
@@ -392,7 +410,7 @@ class API:
autoescape=jinja2.select_autoescape(["html", "xml"] if auto_escape else []),
)
template = env.get_template(name)
template = env.get_template(name_)
return template.render(**values)
def template_string(self, s, auto_escape=True, **values):
+19 -35
View File
@@ -1,17 +1,19 @@
import io
import json
import gzip
from http.cookies import SimpleCookie
import chardet
import rfc3986
import graphene
import yaml
from requests.structures import CaseInsensitiveDict
from requests.cookies import RequestsCookieJar
from starlette.datastructures import MutableHeaders
from starlette.requests import Request as StarletteRequest
from starlette.responses import Response as StarletteResponse
from urllib.parse import parse_qs
from .status_codes import HTTP_200
@@ -88,12 +90,7 @@ class QueryDict(dict):
# TODO: add slots
class Request:
__slots__ = [
"_starlette",
"formats",
"_headers",
"_encoding",
]
__slots__ = ["_starlette", "formats", "_headers", "_encoding"]
def __init__(self, scope, receive):
self._starlette = StarletteRequest(scope, receive)
@@ -130,6 +127,18 @@ class Request:
"""The parsed URL of the Request."""
return rfc3986.urlparse(self.full_url)
@property
def cookies(self):
cookies = RequestsCookieJar()
cookie_header = self.headers.get("cookie", "")
# if cookie_header:
bc = SimpleCookie(cookie_header)
for k, v in bc.items():
cookies[k] = v
return cookies.get_dict()
@property
def params(self):
"""A dictionary of the parsed query parameters used for the Request."""
@@ -191,7 +200,7 @@ class Request:
return content_type in self.headers.get("Accept", [])
async def media(self, format=None):
"""Renders incoming json/yaml/form data as Python objects.
"""Renders incoming json/yaml/form data as Python objects. Must be awaited.
:param format: The name of the format being used. Alternatively accepts a custom callable for the format type.
"""
@@ -216,6 +225,7 @@ class Response:
"media",
"headers",
"formats",
"cookies",
]
def __init__(self, req, *, formats):
@@ -231,6 +241,7 @@ class Response:
{}
) #: A Python dictionary of {Key: value}, representing the headers of the response.
self.formats = formats
self.cookies = {} # req.cookies
@property
async def body(self):
@@ -250,35 +261,8 @@ class Response:
{"Content-Type": "application/json"},
)
@property
async def gzipped_body(self):
body, headers = await self.body
if isinstance(body, str):
body = body.encode(self.encoding)
if "gzip" in self.req.headers["Accept-Encoding"].lower():
gzip_buffer = io.BytesIO()
gzip_file = gzip.GzipFile(mode="wb", fileobj=gzip_buffer)
gzip_file.write(body)
gzip_file.close()
new_headers = {
"Content-Encoding": "gzip",
"Vary": "Accept-Encoding",
"Content-Length": str(len(body)),
}
headers.update(new_headers)
return (gzip_buffer.getvalue(), headers)
else:
return (body, headers)
async def __call__(self, receive, send):
body, headers = await self.body
if len(await self.body) > 500:
body, headers = await self.gzipped_body
if self.headers:
headers.update(self.headers)
+6 -3
View File
@@ -1,5 +1,5 @@
import re
from parse import parse, search
from parse import parse
def memoize(f):
@@ -13,6 +13,8 @@ def memoize(f):
class Route:
_param_pattern = re.compile(r"{([^{}]*)}")
def __init__(self, route, endpoint):
self.route = route
self.endpoint = endpoint
@@ -35,7 +37,7 @@ class Route:
@property
def has_parameters(self):
return all([("{" in self.route), ("}" in self.route)])
return bool(self._param_pattern.search(self.route))
@memoize
def does_match(self, s):
@@ -58,7 +60,8 @@ class Route:
return url
def _weight(self):
params_count = -len(set(re.findall(r"{([a-zA-Z]\w*)}", self.route)))
params = set(self._param_pattern.findall(self.route))
params_count = -len(params) or 0
return params_count != 0, params_count
@property
+24 -1
View File
@@ -61,6 +61,15 @@ def test_class_based_view_registration(api):
resp.text = "42"
def test_class_based_view_parameters(api):
@api.route("/{greeting}")
class Greeting:
def on_request(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
assert api.session().get("http://;/Hello").ok
def test_requests_session(api):
assert api.session()
@@ -244,12 +253,13 @@ def test_graphql_schema_json_query(api, schema):
r = api.session().post("http://;/", json={"query": "{ hello }"})
assert r.ok
def test_graphiql(api, schema):
api.add_route("/", schema)
r = api.session().get("http://;/", headers={"Accept": "text/html"})
assert r.ok
assert 'GraphiQL' in r.text
assert "GraphiQL" in r.text
def test_json_uploads(api, session):
@@ -359,3 +369,16 @@ def test_async_class_based_views(api, session):
data = "frame"
r = session.post(api.url_for(Resource), data=data)
assert r.text == data
def test_cookies(api, session):
@api.route("/")
def cookies(req, resp):
resp.media = {'cookies': req.cookies}
resp.cookies['sent'] = 'true'
r = session.get(api.url_for(cookies), cookies={'hello': 'universe'})
assert r.json() == {"cookies": {"hello": "universe"}}
assert 'sent' in r.cookies
r = session.get(api.url_for(cookies))
assert r.json() == {"cookies": {"sent": "true"}}
+48 -26
View File
@@ -30,32 +30,32 @@ def test_equal():
assert r != r3
@pytest.mark.parametrize(
"path_param, actual, match",
[
pytest.param(
"/{greetings}", "/hello", {"greetings": "hello"}, id="with one strformat"
),
pytest.param(
"/{greetings}.{name}",
"/hi.jane",
{"greetings": "hi", "name": "jane"},
id="with dot in url and two strformat",
),
pytest.param(
"/{greetings}/{name}",
"/hi/john",
{"greetings": "hi", "name": "john"},
id="with sub url and two strformat",
),
pytest.param(
"/concrete_path", "/foo", {}, id="test concrete path with no match"
),
],
)
def test_incoming_matches(path_param, actual, match):
r = routes.Route(path_param, "test_endpoint")
assert r.incoming_matches(actual) == match
def test_incoming_matches():
# Test Route with one param
r = routes.Route("/{greetings}", "test_endpoint")
assert r.incoming_matches("/hello") == {"greetings": "hello"}
assert r.incoming_matches("/foo") == {"greetings": "foo"}
assert r._memo == {
"incoming_matches:/hello": {"greetings": "hello"},
"incoming_matches:/foo": {"greetings": "foo"},
}
# Test Route with two params
r = routes.Route("/{greetings}/{name}", "test_endpoint")
assert r.incoming_matches("/hi/john") == {"greetings": "hi", "name": "john"}
assert r.incoming_matches("/hello/jane") == {"greetings": "hello", "name": "jane"}
# Test Route with no param
assert r._memo == {
"incoming_matches:/hi/john": {"greetings": "hi", "name": "john"},
"incoming_matches:/hello/jane": {"greetings": "hello", "name": "jane"},
}
r = routes.Route("/hello", "test_endpoint")
assert r.incoming_matches("/hello") == {}
assert r.incoming_matches("/bye") == {}
assert r._memo == {"incoming_matches:/hello": {}, "incoming_matches:/bye": {}}
def test_incoming_matches_with_concrete_path_no_match():
@@ -81,3 +81,25 @@ def test_incoming_matches_with_concrete_path_no_match():
def test_does_match_with_route(route, match, expected):
r = routes.Route(route, "test_endpoint")
assert r.does_match(match) == expected
@pytest.mark.parametrize(
"path_param, expected_weight",
[
pytest.param("/{greetings}", (True, -1), id="with one param"),
pytest.param(
"/{greetings}.{name}", (True, -2), id="with 2 params and dot in the middle"
),
pytest.param("/{greetings}/{name}", (True, -2), id="with 2 param and subpath"),
pytest.param(
"/{greetings}/{name}/{hello}", (True, -3), id="with 3 param and subpath"
),
pytest.param(
"/{greetings}_{name}", (True, -2), id="with 2 param and underscore"
),
pytest.param("/hello", (False, 0), id="with 2 param and underscore"),
],
)
def test_weight(path_param, expected_weight):
r = routes.Route(path_param, "test_endpoint")
assert r._weight() == expected_weight
+67
View File
@@ -0,0 +1,67 @@
import pytest
from responder import status_codes
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(101, True, id="Normal 101"),
pytest.param(199, True, id="Not actual status code but within 100"),
pytest.param(0, False, id="Zero case (below 100)"),
pytest.param(200, False, id="Above 100")
],
)
def test_is_100(status_code, expected):
assert status_codes.is_100(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(201, True, id="Normal 201"),
pytest.param(299, True, id="Not actual status code but within 200"),
pytest.param(0, False, id="Zero case (below 200)"),
pytest.param(300, False, id="Above 200")
],
)
def test_is_200(status_code, expected):
assert status_codes.is_200(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(301, True, id="Normal 301"),
pytest.param(399, True, id="Not actual status code but within 300"),
pytest.param(0, False, id="Zero case (below 300)"),
pytest.param(400, False, id="Above 300")
],
)
def test_is_300(status_code, expected):
assert status_codes.is_300(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(401, True, id="Normal 401"),
pytest.param(499, True, id="Not actual status code but within 400"),
pytest.param(0, False, id="Zero case (below 400)"),
pytest.param(500, False, id="Above 400")
],
)
def test_is_400(status_code, expected):
assert status_codes.is_400(status_code) is expected
@pytest.mark.parametrize(
"status_code, expected",
[
pytest.param(501, True, id="Normal 401"),
pytest.param(599, True, id="Not actual status code but within 400"),
pytest.param(0, False, id="Zero case (below 400)"),
pytest.param(600, False, id="Above 500")
],
)
def test_is_500(status_code, expected):
assert status_codes.is_500(status_code) is expected