diff --git a/Pipfile b/Pipfile index 079582c..17c70e4 100644 --- a/Pipfile +++ b/Pipfile @@ -11,6 +11,7 @@ pytest = "*" "flake8" = "*" black = "*" twine = "*" +flask = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 565e0d3..2350f30 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "13d6d0b2229e162611f4355a6887db0719a0c47471facb391b524635567d6884" + "sha256": "5843d79d019341544a1c9456b537125203079f127721132c8111421095660524" }, "pipfile-spec": 6, "requires": { @@ -117,7 +117,6 @@ "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" ], - "markers": "python_version < '4' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'", "version": "==1.23" }, "waitress": { @@ -155,7 +154,6 @@ "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" ], - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==1.2.1" }, "attrs": { @@ -175,10 +173,10 @@ }, "bleach": { "hashes": [ - "sha256:9c471c0dd9c820f6bf4ee5ca3e348ceccefbc1475d9a40c397ed5d04e0b42c54", - "sha256:b407b2612b37e6cdc6704f84cec18c1f140b78e6c625652a844e89d6b9855f6b" + "sha256:c39d25d9ada62009853b0281efdc35a792db8cdee89465433e6d0aaaf5defc3f", + "sha256:f680cc08e2eea821f3173b875f68763960006f1e764c92b5d2b8354af3a47468" ], - "version": "==3.0.0" + "version": "==3.0.1" }, "certifi": { "hashes": [ @@ -236,7 +234,6 @@ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], - "markers": "python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.7'", "version": "==7.0" }, "cmarkgfm": { @@ -270,7 +267,6 @@ "sha256:e8932bddf159064f04e946fbb64693753488de21586f20e840b3be51745c8c09", "sha256:f20900f16377f2109783ae9348d34bc80530808439591c3d3df73d5c7ef1a00c" ], - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==0.4.2" }, "colorama": { @@ -297,6 +293,14 @@ "index": "pypi", "version": "==3.5.0" }, + "flask": { + "hashes": [ + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + ], + "index": "pypi", + "version": "==1.0.2" + }, "future": { "hashes": [ "sha256:e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb" @@ -310,6 +314,25 @@ ], "version": "==2.7" }, + "itsdangerous": { + "hashes": [ + "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" + ], + "version": "==0.24" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "markupsafe": { + "hashes": [ + "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" + ], + "version": "==1.0" + }, "mccabe": { "hashes": [ "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", @@ -337,7 +360,6 @@ "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" ], - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==0.7.1" }, "py": { @@ -345,7 +367,6 @@ "sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1", "sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6" ], - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==1.6.0" }, "pycodestyle": { @@ -359,7 +380,6 @@ "hashes": [ "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" ], - "markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.3.*'", "version": "==2.19" }, "pyflakes": { @@ -389,7 +409,6 @@ "sha256:237ca8705ffea849870de41101dba41543561da05c0ae45b2f1c547efa9843d2", "sha256:f75049a3a7afa57165551e030dd8f9882ebf688b9600535a3f7e23596651875d" ], - "markers": "python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==22.0" }, "requests": { @@ -425,7 +444,6 @@ "sha256:18f1818ce951aeb9ea162ae1098b43f583f7d057b34d706f66939353d1208889", "sha256:df02c0650160986bac0218bb07952245fc6960d23654648b5d5526ad5a4128c9" ], - "markers": "python_version >= '2.6' and python_version != '3.0.*' and python_version != '3.1.*'", "version": "==4.26.0" }, "twine": { @@ -441,7 +459,6 @@ "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" ], - "markers": "python_version < '4' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'", "version": "==1.23" }, "webencodings": { @@ -450,6 +467,13 @@ "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" ], "version": "==0.5.1" + }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" } } } diff --git a/README.md b/README.md index 9351922..3eea327 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ The primary concept here is to bring the nicities that are brought forth from bo ## New Ideas - **A built in testing client that uses the actual Requests you know and love**. +- The ability to mount other WSGI apps easily. - Automatic gzipped-responses (still working on that). - In addition to Falcon's `on_get`, `on_post`, etc methods, Responder features an `on_request` method, which gets called on every type of request, much like Requests. - WhiteNoise is built-in, for serving static files (this has yet to be built out, there's no templating or `static_url` yet) diff --git a/app.py b/app.py index 66c6f94..e4db9c8 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,19 @@ import responder import graphene + +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def hello_world(): + return "Hello, World from flask!" + + api = responder.API() -# api.mount('/subapp', other_wsgi_app) +api.mount("/hello", app) @api.route("/") @@ -44,7 +55,7 @@ print( print( api.session() .get( - "http://app/graph", + "http://app/hello/", data="{ hello }", headers={"Accept": "application/x-yaml"}, # data="hello", @@ -53,4 +64,4 @@ print( ) # {hello: Hello stranger} -api.run(port=5000, expose_tracebacks=True) +# api.run(port=5000, expose_tracebacks=True) diff --git a/responder/api.py b/responder/api.py index af3105c..9f0daa1 100644 --- a/responder/api.py +++ b/responder/api.py @@ -6,18 +6,28 @@ import waitress from whitenoise import WhiteNoise from wsgiadapter import WSGIAdapter as RequestsWSGIAdapter from requests import Session as RequestsSession +from werkzeug.wsgi import DispatcherMiddleware from . import models from .status import HTTP_404 -class BaseAPI: - __slots__ = ["routes"] - - def __init__(self): +class API: + def __init__(self, static="static"): + self.static_dir = Path(os.path.abspath(static)) self.routes = {} + self.apps = {"/": self._wsgi_app} - def _wsgi_app(self, environ, start_response): + # Make the static directory if it doesn't exist. + os.makedirs(self.static_dir, exist_ok=True) + + # Mount the whitenoise application. + self.whitenoise = WhiteNoise(self.__wsgi_app, root=str(self.static_dir)) + + # Cached requests session. + self._session = None + + def __wsgi_app(self, environ, start_response): # def wsgi_app(self, request): """The actual WSGI application. This is not implemented in :meth:`__call__` so that middlewares can be applied without @@ -44,14 +54,20 @@ class BaseAPI: start the response. """ - req = models.Request.from_environ(environ) + req = models.Request.from_environ(environ, start_response) + # if not req.dispatched: resp = self._dispatch_request(req) - return resp(environ, start_response) - def wsgi_app(self, environ, start_response): + def _wsgi_app(self, environ, start_response): return self.whitenoise(environ, start_response) + def wsgi_app(self, environ, start_response): + apps = self.apps.copy() + main = apps.pop("/") + + return DispatcherMiddleware(main, apps)(environ, start_response) + def __call__(self, environ, start_response): """The WSGI server calls the Flask application object as the WSGI application. This calls :meth:`wsgi_app` which can be @@ -74,10 +90,18 @@ class BaseAPI: except TypeError: try: view = self.routes[route]() - # GraphQL Schema. except TypeError: view = self.routes[route] - self.graphql_response(req, resp, schema=view) + try: + # GraphQL Schema. + assert hasattr(view, "execute") + self.graphql_response(req, resp, schema=view) + except AssertionError: + # WSGI App. + req.dispatched = True + return view( + environ=req._environ, start_response=req._start_response + ) # Run on_request first. try: @@ -97,25 +121,6 @@ class BaseAPI: return resp - @property - def static_dir(self): - return Path(".") - - -class API(BaseAPI): - __slots__ = ("routes", "_session", "whitenoise", "static_dir") - - def __init__(self, static="static"): - super().__init__() - self._session = None - self.static_dir = Path(os.path.abspath(static)) - - # Make the static directory if it doesn't exist. - os.makedirs(self.static_dir, exist_ok=True) - - # Mount the whitenoise application. - self.whitenoise = WhiteNoise(self._wsgi_app, root=str(self.static_dir)) - def add_route(self, route, view, *, check_existing=True, graphiql=False): if check_existing: assert route not in self.routes @@ -158,6 +163,9 @@ class API(BaseAPI): return decorator + def mount(self, route, wsgi_app): + self.apps.update({route: wsgi_app}) + def session(self, base_url="http://app"): if self._session is None: session = RequestsSession() diff --git a/responder/models.py b/responder/models.py index 4f7c81a..ee8d012 100644 --- a/responder/models.py +++ b/responder/models.py @@ -8,7 +8,7 @@ from requests.models import Request as RequestsRequest from requests.structures import CaseInsensitiveDict from werkzeug.wrappers import Request as WerkzeugRequest from werkzeug.wrappers import BaseResponse as WerkzeugResponse - +from werkzeug.wsgi import DispatcherMiddleware from urllib.parse import parse_qs @@ -25,7 +25,7 @@ class Request: self._wz = None @classmethod - def from_environ(kls, environ): + def from_environ(kls, environ, start_response=None): self = kls() self._wz = WerkzeugRequest(environ) self.headers = CaseInsensitiveDict(self._wz.headers.to_list()) @@ -39,6 +39,9 @@ class Request: self.content = self._wz.get_data(cache=True, as_text=False) self.text = self._wz.get_data(cache=True, as_text=True) self.data = self._wz.get_data(cache=True, as_text=True, parse_form_data=True) + self.dispatched = False + self._start_response = start_response + self._environ = environ return self