mirror of
https://github.com/kennethreitz/responder.git
synced 2026-06-05 23:00:17 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a46a87b3e | |||
| 0678daa880 | |||
| 6761e3bdd8 | |||
| ead213a506 | |||
| 75b5782eee | |||
| a80df809e4 | |||
| 7f3177f662 | |||
| 906cd2fbbf | |||
| 9d0129da56 | |||
| aedcf12d99 | |||
| 86361523e2 | |||
| a7110ef441 | |||
| d3e4968546 | |||
| 03e34d56ab | |||
| b470d10416 |
+25
-1
@@ -5,6 +5,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
## [v2.0.4] - 2019-11-19
|
||||||
|
### Fixed
|
||||||
|
- Fix static app resolving
|
||||||
|
|
||||||
|
## [v2.0.3] - 2019-09-20
|
||||||
|
### Fixed
|
||||||
|
- Fix template conflicts
|
||||||
|
|
||||||
|
## [v2.0.2] - 2019-09-20
|
||||||
|
### Fixed
|
||||||
|
- Fix template conflicts
|
||||||
|
|
||||||
|
## [v2.0.1] - 2019-09-20
|
||||||
|
### Fixed
|
||||||
|
- Fix template import
|
||||||
|
|
||||||
|
## [v2.0.0] - 2019-09-19
|
||||||
|
### Changed
|
||||||
|
- Refactor Router and Schema
|
||||||
|
|
||||||
## [v1.3.2] - 2019-08-15
|
## [v1.3.2] - 2019-08-15
|
||||||
### Added
|
### Added
|
||||||
@@ -208,7 +227,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
### Added
|
### Added
|
||||||
- Conception!
|
- Conception!
|
||||||
|
|
||||||
[Unreleased]: https://github.com/taoufik07/responder/compare/v1.3.2..HEAD
|
[Unreleased]: https://github.com/taoufik07/responder/compare/v2.0.4..HEAD
|
||||||
|
[v2.0.4]: https://github.com/taoufik07/responder/compare/v2.0.3..v2.0.4
|
||||||
|
[v2.0.3]: https://github.com/taoufik07/responder/compare/v2.0.2..v2.0.3
|
||||||
|
[v2.0.2]: https://github.com/taoufik07/responder/compare/v2.0.1..v2.0.2
|
||||||
|
[v2.0.1]: https://github.com/taoufik07/responder/compare/v2.0.0..v2.0.1
|
||||||
|
[v2.0.0]: https://github.com/taoufik07/responder/compare/v1.3.2..v2.0.0
|
||||||
[v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
|
[v1.3.2]: https://github.com/taoufik07/responder/compare/v1.3.1..v1.3.2
|
||||||
[v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
|
[v1.3.1]: https://github.com/taoufik07/responder/compare/v1.3.0..v1.3.1
|
||||||
[v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
|
[v1.3.0]: https://github.com/taoufik07/responder/compare/v1.2.0..v1.3.0
|
||||||
|
|||||||
Generated
+7
@@ -116,6 +116,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.8"
|
"version": "==2.8"
|
||||||
},
|
},
|
||||||
|
"itsdangerous": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
|
||||||
|
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
|
||||||
|
],
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
"jinja2": {
|
"jinja2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
|
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ Usage::
|
|||||||
|
|
||||||
Also a ``render_async`` is available::
|
Also a ``render_async`` is available::
|
||||||
|
|
||||||
|
templates = Templates(enable_async=True)
|
||||||
resp.html = await templates.render_async("hello.html", who=who)
|
resp.html = await templates.render_async("hello.html", who=who)
|
||||||
|
|
||||||
You can also use the existing ``api.template(filename, *args, **kwargs)`` to render templates::
|
You can also use the existing ``api.template(filename, *args, **kwargs)`` to render templates::
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "1.3.2"
|
__version__ = "2.0.4"
|
||||||
|
|||||||
+8
-31
@@ -44,12 +44,13 @@ class API:
|
|||||||
*,
|
*,
|
||||||
debug=False,
|
debug=False,
|
||||||
title=None,
|
title=None,
|
||||||
version=None,
|
version="1.0",
|
||||||
description=None,
|
description=None,
|
||||||
terms_of_service=None,
|
terms_of_service=None,
|
||||||
contact=None,
|
contact=None,
|
||||||
license=None,
|
license=None,
|
||||||
openapi=None,
|
openapi=None,
|
||||||
|
openapi_version="3.0.2",
|
||||||
openapi_route="/schema.yml",
|
openapi_route="/schema.yml",
|
||||||
static_dir="static",
|
static_dir="static",
|
||||||
static_route="/static",
|
static_route="/static",
|
||||||
@@ -76,15 +77,6 @@ class API:
|
|||||||
self.static_dir = static_dir
|
self.static_dir = static_dir
|
||||||
self.static_route = static_route
|
self.static_route = static_route
|
||||||
|
|
||||||
self.built_in_templates_dir = Path(
|
|
||||||
os.path.abspath(os.path.dirname(__file__) + "/templates")
|
|
||||||
)
|
|
||||||
|
|
||||||
if templates_dir is not None:
|
|
||||||
templates_dir = Path(os.path.abspath(templates_dir))
|
|
||||||
|
|
||||||
self.templates_dir = templates_dir or self.built_in_templates_dir
|
|
||||||
|
|
||||||
self.hsts_enabled = enable_hsts
|
self.hsts_enabled = enable_hsts
|
||||||
self.cors = cors
|
self.cors = cors
|
||||||
self.cors_params = cors_params
|
self.cors_params = cors_params
|
||||||
@@ -98,10 +90,8 @@ class API:
|
|||||||
allowed_hosts = ["*"]
|
allowed_hosts = ["*"]
|
||||||
self.allowed_hosts = allowed_hosts
|
self.allowed_hosts = allowed_hosts
|
||||||
|
|
||||||
# Make the static/templates directory if they don't exist.
|
if self.static_dir is not None:
|
||||||
for _dir in (self.static_dir, self.templates_dir):
|
os.makedirs(self.static_dir, exist_ok=True)
|
||||||
if _dir is not None:
|
|
||||||
os.makedirs(_dir, exist_ok=True)
|
|
||||||
|
|
||||||
if self.static_dir is not None:
|
if self.static_dir is not None:
|
||||||
self.mount(self.static_route, self.static_app)
|
self.mount(self.static_route, self.static_app)
|
||||||
@@ -128,9 +118,9 @@ class API:
|
|||||||
if openapi or docs_route:
|
if openapi or docs_route:
|
||||||
self.openapi = OpenAPISchema(
|
self.openapi = OpenAPISchema(
|
||||||
app=self,
|
app=self,
|
||||||
title="Web Service",
|
title=title,
|
||||||
version="1.0",
|
version=version,
|
||||||
openapi="3.0.2",
|
openapi=openapi_version,
|
||||||
docs_route=docs_route,
|
docs_route=docs_route,
|
||||||
description=description,
|
description=description,
|
||||||
terms_of_service=terms_of_service,
|
terms_of_service=terms_of_service,
|
||||||
@@ -153,11 +143,6 @@ class API:
|
|||||||
self._static_app = StaticFiles(directory=self.static_dir)
|
self._static_app = StaticFiles(directory=self.static_dir)
|
||||||
return self._static_app
|
return self._static_app
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _notfound_wsgi_app(environ, start_response):
|
|
||||||
start_response("404 NOT FOUND", [("Content-Type", "text/plain")])
|
|
||||||
return [b"Not Found."]
|
|
||||||
|
|
||||||
def before_request(self, websocket=False):
|
def before_request(self, websocket=False):
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
self.router.before_request(f, websocket=websocket)
|
self.router.before_request(f, websocket=websocket)
|
||||||
@@ -165,14 +150,6 @@ class API:
|
|||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
@property
|
|
||||||
def before_http_requests(self):
|
|
||||||
return self.before_requests.get("http", [])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def before_ws_requests(self):
|
|
||||||
return self.before_requests.get("ws", [])
|
|
||||||
|
|
||||||
def add_middleware(self, middleware_cls, **middleware_config):
|
def add_middleware(self, middleware_cls, **middleware_config):
|
||||||
self.app = middleware_cls(self.app, **middleware_config)
|
self.app = middleware_cls(self.app, **middleware_config)
|
||||||
|
|
||||||
@@ -242,7 +219,7 @@ class API:
|
|||||||
index = (self.static_dir / "index.html").resolve()
|
index = (self.static_dir / "index.html").resolve()
|
||||||
if os.path.exists(index):
|
if os.path.exists(index):
|
||||||
with open(index, "r") as f:
|
with open(index, "r") as f:
|
||||||
resp.html = "Hello world !"
|
resp.html = f.read()
|
||||||
else:
|
else:
|
||||||
resp.status_code = status_codes.HTTP_404
|
resp.status_code = status_codes.HTTP_404
|
||||||
resp.text = "Not found."
|
resp.text = "Not found."
|
||||||
|
|||||||
+1
-3
@@ -283,9 +283,7 @@ class Response:
|
|||||||
self.content = None #: A bytes representation of the response body.
|
self.content = None #: A bytes representation of the response body.
|
||||||
self.mimetype = None
|
self.mimetype = None
|
||||||
self.encoding = DEFAULT_ENCODING
|
self.encoding = DEFAULT_ENCODING
|
||||||
self.media = (
|
self.media = None #: A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
|
||||||
None
|
|
||||||
) #: A Python object that will be content-negotiated and sent back to the client. Typically, in JSON formatting.
|
|
||||||
self._stream = None
|
self._stream = None
|
||||||
self.headers = (
|
self.headers = (
|
||||||
{}
|
{}
|
||||||
|
|||||||
+9
-8
@@ -302,6 +302,15 @@ class Router:
|
|||||||
path = scope["path"]
|
path = scope["path"]
|
||||||
root_path = scope.get("root_path", "")
|
root_path = scope.get("root_path", "")
|
||||||
|
|
||||||
|
# Check "primary" mounted routes first (before submounted apps)
|
||||||
|
route = self._resolve_route(scope)
|
||||||
|
|
||||||
|
scope["before_requests"] = self.before_requests
|
||||||
|
|
||||||
|
if route is not None:
|
||||||
|
await route(scope, receive, send)
|
||||||
|
return
|
||||||
|
|
||||||
# Call into a submounted app, if one exists.
|
# Call into a submounted app, if one exists.
|
||||||
for path_prefix, app in self.apps.items():
|
for path_prefix, app in self.apps.items():
|
||||||
if path.startswith(path_prefix):
|
if path.startswith(path_prefix):
|
||||||
@@ -315,12 +324,4 @@ class Router:
|
|||||||
await app(scope, receive, send)
|
await app(scope, receive, send)
|
||||||
return
|
return
|
||||||
|
|
||||||
route = self._resolve_route(scope)
|
|
||||||
|
|
||||||
scope["before_requests"] = self.before_requests
|
|
||||||
|
|
||||||
if route is not None:
|
|
||||||
await route(scope, receive, send)
|
|
||||||
return
|
|
||||||
|
|
||||||
await self.default_response(scope, receive, send)
|
await self.default_response(scope, receive, send)
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import jinja2
|
|||||||
|
|
||||||
|
|
||||||
class Templates:
|
class Templates:
|
||||||
def __init__(self, directory="templates", autoescape=True, context=None):
|
def __init__(
|
||||||
|
self, directory="templates", autoescape=True, context=None, enable_async=False
|
||||||
|
):
|
||||||
self.directory = directory
|
self.directory = directory
|
||||||
self._env = jinja2.Environment(
|
self._env = jinja2.Environment(
|
||||||
loader=jinja2.FileSystemLoader([str(self.directory)]), autoescape=autoescape
|
loader=jinja2.FileSystemLoader([str(self.directory)]),
|
||||||
|
autoescape=autoescape,
|
||||||
|
enable_async=enable_async,
|
||||||
)
|
)
|
||||||
self.default_context = {} if context is None else {**context}
|
self.default_context = {} if context is None else {**context}
|
||||||
self._env.globals.update(self.default_context)
|
self._env.globals.update(self.default_context)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ required = [
|
|||||||
"docopt",
|
"docopt",
|
||||||
"requests-toolbelt",
|
"requests-toolbelt",
|
||||||
"apistar",
|
"apistar",
|
||||||
|
"itsdangerous",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,3 +56,12 @@ def schema():
|
|||||||
return f"Hello {name}"
|
return f"Hello {name}"
|
||||||
|
|
||||||
return graphene.Schema(query=Query)
|
return graphene.Schema(query=Query)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def template_path(tmpdir):
|
||||||
|
# create a Jinja template file on the filesystem
|
||||||
|
template_name = "test.html"
|
||||||
|
template_file = tmpdir.mkdir("static").join(template_name)
|
||||||
|
template_file.write("{{ var }}")
|
||||||
|
return template_file
|
||||||
|
|||||||
+61
-28
@@ -8,6 +8,7 @@ import requests
|
|||||||
import string
|
import string
|
||||||
import io
|
import io
|
||||||
from responder.routes import Router, Route, WebSocketRoute
|
from responder.routes import Router, Route, WebSocketRoute
|
||||||
|
from responder.templates import Templates
|
||||||
|
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from starlette.responses import PlainTextResponse
|
from starlette.responses import PlainTextResponse
|
||||||
@@ -590,6 +591,39 @@ def test_template_string_rendering(api):
|
|||||||
assert r.text == "hello"
|
assert r.text == "hello"
|
||||||
|
|
||||||
|
|
||||||
|
def test_template_rendering(template_path):
|
||||||
|
api = responder.API(templates_dir=template_path.dirpath())
|
||||||
|
|
||||||
|
@api.route("/")
|
||||||
|
def view(req, resp):
|
||||||
|
resp.content = api.template(template_path.basename, var="hello")
|
||||||
|
|
||||||
|
r = api.requests.get(api.url_for(view))
|
||||||
|
assert r.text == "hello"
|
||||||
|
|
||||||
|
|
||||||
|
def test_template(api, template_path):
|
||||||
|
templates = Templates(directory=template_path.dirpath())
|
||||||
|
|
||||||
|
@api.route("/{var}/")
|
||||||
|
def view(req, resp, var):
|
||||||
|
resp.html = templates.render(template_path.basename, var=var)
|
||||||
|
|
||||||
|
r = api.requests.get("/test/")
|
||||||
|
assert r.text == "test"
|
||||||
|
|
||||||
|
|
||||||
|
def test_template_async(api, template_path):
|
||||||
|
templates = Templates(directory=template_path.dirpath(), enable_async=True)
|
||||||
|
|
||||||
|
@api.route("/{var}/async")
|
||||||
|
async def view_async(req, resp, var):
|
||||||
|
resp.html = await templates.render_async(template_path.basename, var=var)
|
||||||
|
|
||||||
|
r = api.requests.get("/test/async")
|
||||||
|
assert r.text == "test"
|
||||||
|
|
||||||
|
|
||||||
def test_file_uploads(api):
|
def test_file_uploads(api):
|
||||||
@api.route("/")
|
@api.route("/")
|
||||||
async def upload(req, resp):
|
async def upload(req, resp):
|
||||||
@@ -751,8 +785,12 @@ def test_before_response(api, session):
|
|||||||
assert "x-pizza" in r.headers
|
assert "x-pizza" in r.headers
|
||||||
|
|
||||||
|
|
||||||
def test_allowed_hosts():
|
@pytest.mark.parametrize("enable_hsts", [True, False])
|
||||||
api = responder.API(allowed_hosts=[";", "tenant.;"])
|
@pytest.mark.parametrize("cors", [True, False])
|
||||||
|
def test_allowed_hosts(enable_hsts, cors):
|
||||||
|
api = responder.API(
|
||||||
|
allowed_hosts=[";", "tenant.;"], enable_hsts=enable_hsts, cors=cors
|
||||||
|
)
|
||||||
|
|
||||||
@api.route("/")
|
@api.route("/")
|
||||||
def get(req, resp):
|
def get(req, resp):
|
||||||
@@ -816,14 +854,15 @@ def create_asset(static_dir, name=None, parent_dir=None):
|
|||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
|
||||||
def test_staticfiles(tmpdir):
|
@pytest.mark.parametrize("static_route", [None, "/static", "/custom/static/route"])
|
||||||
|
def test_staticfiles(tmpdir, static_route):
|
||||||
static_dir = tmpdir.mkdir("static")
|
static_dir = tmpdir.mkdir("static")
|
||||||
|
|
||||||
asset1 = create_asset(static_dir)
|
asset1 = create_asset(static_dir)
|
||||||
parent_dir = "css"
|
parent_dir = "css"
|
||||||
asset2 = create_asset(static_dir, name="asset2", parent_dir=parent_dir)
|
asset2 = create_asset(static_dir, name="asset2", parent_dir=parent_dir)
|
||||||
|
|
||||||
api = responder.API(static_dir=str(static_dir))
|
api = responder.API(static_dir=str(static_dir), static_route=static_route)
|
||||||
session = api.session()
|
session = api.session()
|
||||||
|
|
||||||
static_route = api.static_route
|
static_route = api.static_route
|
||||||
@@ -847,30 +886,6 @@ def test_staticfiles(tmpdir):
|
|||||||
assert r.status_code == api.status_codes.HTTP_404
|
assert r.status_code == api.status_codes.HTTP_404
|
||||||
|
|
||||||
|
|
||||||
def test_staticfiles_custom_route(tmpdir):
|
|
||||||
static_dir = tmpdir.mkdir("static")
|
|
||||||
static_route = "/custom/static/route"
|
|
||||||
|
|
||||||
asset = create_asset(static_dir)
|
|
||||||
|
|
||||||
api = responder.API(static_dir=str(static_dir), static_route=static_route)
|
|
||||||
session = api.session()
|
|
||||||
|
|
||||||
static_route = api.static_route
|
|
||||||
|
|
||||||
# ok
|
|
||||||
r = session.get(f"{static_route}/{asset.basename}")
|
|
||||||
assert r.status_code == api.status_codes.HTTP_200
|
|
||||||
|
|
||||||
# Asset not found
|
|
||||||
r = session.get(f"{static_route}/not_found.css")
|
|
||||||
assert r.status_code == api.status_codes.HTTP_404
|
|
||||||
|
|
||||||
# Not found on dir listing
|
|
||||||
r = session.get(f"{static_route}")
|
|
||||||
assert r.status_code == api.status_codes.HTTP_404
|
|
||||||
|
|
||||||
|
|
||||||
def test_staticfiles_none_dir(tmpdir):
|
def test_staticfiles_none_dir(tmpdir):
|
||||||
api = responder.API(static_dir=None)
|
api = responder.API(static_dir=None)
|
||||||
session = api.session()
|
session = api.session()
|
||||||
@@ -985,3 +1000,21 @@ def test_empty_req_text(api):
|
|||||||
resp.text = "{}_{}".format(req.state.test2, req.state.test1)
|
resp.text = "{}_{}".format(req.state.test2, req.state.test1)
|
||||||
|
|
||||||
assert api.requests.get(url("/")).text == "Foo_42"
|
assert api.requests.get(url("/")).text == "Foo_42"
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_matches_route(api):
|
||||||
|
@api.route("/hello")
|
||||||
|
def home(req, resp):
|
||||||
|
resp.text = "hello world!"
|
||||||
|
|
||||||
|
route = api.path_matches_route({"type": "http", "path": "/hello"})
|
||||||
|
assert route.endpoint_name == "home"
|
||||||
|
|
||||||
|
assert not api.path_matches_route({"type": "http", "path": "/foo"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_without_endpoint(api):
|
||||||
|
# test that a route without endpoint gets a default static response
|
||||||
|
api.add_route("/")
|
||||||
|
route = api.router.routes[0]
|
||||||
|
assert route.endpoint_name == "_static_response"
|
||||||
|
|||||||
Reference in New Issue
Block a user