diff --git a/Pipfile b/Pipfile index f557888..06bc6de 100644 --- a/Pipfile +++ b/Pipfile @@ -10,6 +10,7 @@ pyyaml = "*" requests = "*" requests-wsgi-adapter = "*" graphene = "*" +whitenoise = "*" [dev-packages] pytest = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 3249ea9..a74c1f5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "35fa1f64486f787f137dd769806ab0b753ec56ae94e6f9ebb953be588c81d3be" + "sha256": "f6b7cc3cf0ac2760ea99bcb8d18c743eff418c6269da29823ccfdbdea19a8c1e" }, "pipfile-spec": 6, "requires": { @@ -135,6 +135,14 @@ ], "index": "pypi", "version": "==0.14.1" + }, + "whitenoise": { + "hashes": [ + "sha256:133a92ff0ab8fb9509f77d4f7d0de493eca19c6fea973f4195d4184f888f2e02", + "sha256:32b57d193478908a48acb66bf73e7a3c18679263e3e64bfebcfac1144a430039" + ], + "index": "pypi", + "version": "==4.1" } }, "develop": { diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc96589 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Responder: a Sorta Familar HTTP Framework for Python + +![](https://farm2.staticflickr.com/1937/30196007887_604e2f10d8_k_d.jpg) + +I'm adept to keep the "for humans" tagline off this project, until it comes out of the prototyping phase. I'm building this to learn, and to have fun -- while, at the same time, trying to bring something new to the table. + +The Python world certianly doesn't need more web frameworks. But, it does need more creativity, so I thought I'd bring some of my ideas to the table and see what I could come up with. + +# The Basic Idea + +The primary concept here is to bring the nicities that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have. I also wanted to take some of the API primitaves that are instilled in the Requests library and put them into a web framework. So, you'll find a lot of parallels here with Requests. + +## Old Ideas + +- Flask-style route expression, with new capabilities -- primarily, the ability to cast a parameter to integers as well as other types that are missing from Flask, all while using Python 3.6+'s new f-string syntax. + +- I love Falcon's "every request and response is passed into to each view and mutated" methodology, especially `response.media`, and have used it here. In addition to supporting JSON, I have decided to support YAML as well, as Kubernetes is slowly taking over the world, and it uses YAML for all the things. Content-negotiation and all that. + +## New Ideas + +- 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) +- Waitress (will-be) built-in as a production web server. I would have chosen Gunicorn, but it doesn't run on Windows. Plus, Waitress serves well to protect against slowloris attacks, making nginx unneccessary in production. +- GraphQL support, via Graphene. The goal here is to eventually have an embedded version of GraphiQL exposable at any route. + +## Future Ideas + +- I want to be able to "mount" any WSGI app into a sub-route. +- Cooke-based sessions are currently an afterthrought, as this is an API framework, but websites are APIs too. +- Potentially support ASGI instead of WSGI. Will the tradeoffs be worth it? This is a question to ask. Procedural code works well for 90% use cases. + +# The Goal + +The primary goal here is to learn, not to get adoption. Though, who knows how these things will pan out. diff --git a/app.py b/app.py index c253c80..fd8cae0 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,7 @@ import responder import graphene -api = responder.API() +api = responder.API(static="static") # api.mount('/subapp', other_wsgi_app) diff --git a/responder/api.py b/responder/api.py index e479b4e..90b087d 100644 --- a/responder/api.py +++ b/responder/api.py @@ -1,5 +1,9 @@ +import os +from pathlib import Path + import graphene +from whitenoise import WhiteNoise from wsgiadapter import WSGIAdapter as RequestsWSGIAdapter from requests import Session as RequestsSession @@ -13,7 +17,7 @@ class BaseAPI: def __init__(self): self.routes = {} - def wsgi_app(self, environ, start_response): + 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 @@ -45,6 +49,9 @@ class BaseAPI: return resp(environ, start_response) + def wsgi_app(self, environ, start_response): + return self.whitenoise(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 @@ -90,18 +97,31 @@ class BaseAPI: return resp + @property + def static_dir(self): + return Path(".") + class API(BaseAPI): - __slots__ = ("routes", "_session") + __slots__ = ("routes", "_session", "whitenoise", "static_dir") - def __init__(self): + def __init__(self, static="static"): super().__init__() self._session = None + self.static_dir = Path(os.path.abspath(static)) - def add_route(self, route, view, *, check_existing=True): + # 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 + # TODO: Support grpahiql. + self.routes[route] = view def default_response(self, req, resp):