Compare commits

...

15 Commits

Author SHA1 Message Date
taoufik 8a46a87b3e Fix missing openapi title, version and openapi_version and black 2019-12-03 19:38:12 +01:00
taoufik 0678daa880 Bump to v2.0.4 2019-11-19 17:35:16 +01:00
Taoufik 6761e3bdd8 Merge pull request #406 from daphil19/master
fix issues related to using `static=true` in `api.add_route()` and `static_route` in `responder.API()`
2019-11-19 17:30:22 +01:00
David Phillips ead213a506 responder now checks routes added via add_route before mounted routes
routes added by `add_route` (including @api.route()) are now checked
before routes that were added by api.mount()

this especially helps cases for situations where the static route
defined by the API class is '/' but the default route is '/' as well
2019-11-02 13:57:39 -04:00
David Phillips 75b5782eee fix static_response endpoint to return index.html contents 2019-11-02 13:55:34 -04:00
Taoufik a80df809e4 Merge pull request #399 from vbarbaresi/fix_render_async
Async templates require enable_async in jinja2 Environment
2019-10-25 22:19:04 +01:00
taoufik 7f3177f662 v2.0.3 2019-10-22 10:41:34 +01:00
taoufik 906cd2fbbf Fix templates conflicts 2019-10-20 12:48:02 +01:00
taoufik 9d0129da56 Fix templates conflicts 2019-10-20 12:39:32 +01:00
taoufik aedcf12d99 v2.0.1 2019-10-20 12:20:54 +01:00
Vincent Barbaresi 86361523e2 fix async templates rendering requiring enable_async in jinja2
When trying to test the render_async() feature I realized that it wasn't working
The template instance needs to be created with enable_async=True
2019-10-19 14:49:50 +02:00
Taoufik a7110ef441 Merge pull request #395 from vbarbaresi/master
add a few tests for API and remove some dead code
2019-10-19 13:06:01 +01:00
Vincent Barbaresi d3e4968546 add a few tests for API and remove some dead code
Increasing the coverage of api.py to 86%

- use pytest.parametrize to test cors and hsts parameters usage
- use pytest.parametrize to factorize test_staticfiles_custom_route
- add tests for path_matches_route and Route() with no endpoint
- test Jinja template rendering from a file

- remove   _notfound_wsgi_app, before_ws_requests, before_http_requests

they didn't seem used anywhere and the before_* method were broken, referring to
self.before_requests that doesn't exist anymore
2019-10-19 13:47:39 +02:00
Taoufik 03e34d56ab Merge pull request #398 from taoufik07/v2
v2.0.0
2019-10-19 12:22:32 +01:00
taoufik b470d10416 v2.0.0 2019-10-19 12:21:06 +01:00
11 changed files with 130 additions and 75 deletions
+25 -1
View File
@@ -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
View File
@@ -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",
+1
View File
@@ -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
View File
@@ -1 +1 @@
__version__ = "1.3.2" __version__ = "2.0.4"
+8 -31
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+6 -2
View File
@@ -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)
+1
View File
@@ -40,6 +40,7 @@ required = [
"docopt", "docopt",
"requests-toolbelt", "requests-toolbelt",
"apistar", "apistar",
"itsdangerous",
] ]
+9
View File
@@ -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
View File
@@ -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"