From 83fa6d6897bf48b5340cd00fa09807075a2edf1d Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Thu, 11 Oct 2018 10:29:15 -0400 Subject: [PATCH] parameterized routes working --- Pipfile | 1 + Pipfile.lock | 61 ++++++++++++++++++++++++++++++++++++++++++--- app.py | 2 +- responder/api.py | 30 +++++++++++----------- responder/routes.py | 42 +++++++++++++++++++++++++++++++ setup.py | 1 + test_responder.py | 15 +++++------ 7 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 responder/routes.py diff --git a/Pipfile b/Pipfile index 17c70e4..6f4c9dd 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,7 @@ name = "pypi" [packages] responder = {editable = true, path = "."} +uvicorn = "*" [dev-packages] pytest = "*" diff --git a/Pipfile.lock b/Pipfile.lock index cd39c17..3a898bb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5843d79d019341544a1c9456b537125203079f127721132c8111421095660524" + "sha256": "04a8f69fdffabcc25d69228af2c581260027423940c5ff741c42ba752ad2e35d" }, "pipfile-spec": 6, "requires": { @@ -37,6 +37,13 @@ ], "version": "==3.0.4" }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, "graphene": { "hashes": [ "sha256:b8ec446d17fa68721636eaad3d6adc1a378cb6323e219814c8f98c9928fc9642", @@ -63,6 +70,13 @@ ], "version": "==1.1.1" }, + "h11": { + "hashes": [ + "sha256:acca6a44cb52a32ab442b1779adf0875c443c689e9e028f8d831a3769f9c5208", + "sha256:f2b1ca39bfed357d1f19ac732913d5f9faa54a5062eca7d2ec3a916cfb7ae4c7" + ], + "version": "==0.8.1" + }, "idna": { "hashes": [ "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", @@ -83,6 +97,12 @@ ], "version": "==1.0" }, + "parse": { + "hashes": [ + "sha256:9dd6048ea212cd032a342f9f6aa2b7bc222f7407c7e37bdc2777fecd36897437" + ], + "version": "==1.9.0" + }, "promise": { "hashes": [ "sha256:2ebbfc10b7abf6354403ed785fe4f04b9dfd421eb1a474ac8d187022228332af", @@ -138,6 +158,13 @@ ], "version": "==1.23" }, + "uvicorn": { + "hashes": [ + "sha256:8de03999a936d8704f07cc3b1d3a3edb6922a068b64d84b4f5e49604c8b70a11" + ], + "index": "pypi", + "version": "==0.3.12" + }, "waitress": { "hashes": [ "sha256:40b0f297a7f3af61fbfbdc67e59090c70dc150a1601c39ecc9f5f1d283fb931b", @@ -145,6 +172,32 @@ ], "version": "==1.1.0" }, + "websockets": { + "hashes": [ + "sha256:0e2f7d6567838369af074f0ef4d0b802d19fa1fee135d864acc656ceefa33136", + "sha256:2a16dac282b2fdae75178d0ed3d5b9bc3258dabfae50196cbb30578d84b6f6a6", + "sha256:5a1fa6072405648cb5b3688e9ed3b94be683ce4a4e5723e6f5d34859dee495c1", + "sha256:5c1f55a1274df9d6a37553fef8cff2958515438c58920897675c9bc70f5a0538", + "sha256:669d1e46f165e0ad152ed8197f7edead22854a6c90419f544e0f234cc9dac6c4", + "sha256:695e34c4dbea18d09ab2c258994a8bf6a09564e762655408241f6a14592d2908", + "sha256:6b2e03d69afa8d20253455e67b64de1a82ff8612db105113cccec35d3f8429f0", + "sha256:79ca7cdda7ad4e3663ea3c43bfa8637fc5d5604c7737f19a8964781abbd1148d", + "sha256:7fd2dd9a856f72e6ed06f82facfce01d119b88457cd4b47b7ae501e8e11eba9c", + "sha256:82c0354ac39379d836719a77ee360ef865377aa6fdead87909d50248d0f05f4d", + "sha256:8f3b956d11c5b301206382726210dc1d3bee1a9ccf7aadf895aaf31f71c3716c", + "sha256:91ec98640220ae05b34b79ee88abf27f97ef7c61cf525eec57ea8fcea9f7dddb", + "sha256:952be9540d83dba815569d5cb5f31708801e0bbfc3a8c5aef1890b57ed7e58bf", + "sha256:99ac266af38ba1b1fe13975aea01ac0e14bb5f3a3200d2c69f05385768b8568e", + "sha256:9fa122e7adb24232247f8a89f2d9070bf64b7869daf93ac5e19546b409e47e96", + "sha256:a0873eadc4b8ca93e2e848d490809e0123eea154aa44ecd0109c4d0171869584", + "sha256:cb998bd4d93af46b8b49ecf5a72c0a98e5cc6d57fdca6527ba78ad89d6606484", + "sha256:e02e57346f6a68523e3c43bbdf35dde5c440318d1f827208ae455f6a2ace446d", + "sha256:e79a5a896bcee7fff24a788d72e5c69f13e61369d055f28113e71945a7eb1559", + "sha256:ee55eb6bcf23ecc975e6b47c127c201b913598f38b6a300075f84eeef2d3baff", + "sha256:f1414e6cbcea8d22843e7eafdfdfae3dd1aba41d1945f6ca66e4806c07c4f454" + ], + "version": "==6.0" + }, "werkzeug": { "hashes": [ "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", @@ -290,11 +343,11 @@ }, "colorama": { "hashes": [ - "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", - "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" + "sha256:a3d89af5db9e9806a779a50296b5fdb466e281147c2c235e8225ecc6dbf7bbf3", + "sha256:c9b54bebe91a6a803e0772c8561d53f2926bfeb17cd141fbabcb08424086595c" ], "markers": "sys_platform == 'win32'", - "version": "==0.3.9" + "version": "==0.4.0" }, "docutils": { "hashes": [ diff --git a/app.py b/app.py index 26ee723..f92bbf4 100644 --- a/app.py +++ b/app.py @@ -49,7 +49,7 @@ print( # headers={"Accept": "application/x-yaml"}, # data="hello", ) - .headers + .text ) # print( diff --git a/responder/api.py b/responder/api.py index 64f3f9d..fe415a2 100644 --- a/responder/api.py +++ b/responder/api.py @@ -14,8 +14,9 @@ from graphql_server import encode_execution_results, json_encode, default_format from . import models from .status_codes import HTTP_404 +from .routes import Route - +# TODO: consider moving status codes here class API: def __init__(self, static_dir="static", templates_dir="templates"): self.static_dir = Path(os.path.abspath(static_dir)) @@ -81,8 +82,8 @@ class API: return self.wsgi_app(environ, start_response) def path_matches_route(self, url): - for (route, view) in self.routes.items(): - if url == route: + for (route, route_object) in self.routes.items(): + if route_object.does_match(url): return route def _dispatch_request(self, req): @@ -91,13 +92,14 @@ class API: if route: try: - self.routes[route](req, resp) + params = self.routes[route].incoming_matches(req.path) + self.routes[route].endpoint(req, resp, **params) # The request is using class-based views. except TypeError: try: - view = self.routes[route]() + view = self.routes[route].endpoint(**params) except TypeError: - view = self.routes[route] + view = self.routes[route].endpoint try: # GraphQL Schema. assert hasattr(view, "execute") @@ -119,7 +121,7 @@ class API: pass # Then on_get. - method = req.method.lower() + method = req.method try: getattr(view, f"on_{method}")(req, resp) @@ -135,7 +137,7 @@ class API: assert route not in self.routes # TODO: Support grpahiql. - self.routes[route] = view + self.routes[route] = Route(route, view) def default_response(self, req, resp): resp.status_code = HTTP_404 @@ -193,14 +195,13 @@ class API: return self._session def url_for(self, view, absolute_url=False, **params): - for (route, _view) in self.routes.items(): - if view == _view: - # TODO: Lots of cleanup here. - return route + for (route, route_object) in self.routes.items(): + if route_object.endpoint == _view: + return route_object.url(**params) raise ValueError def url(self): - # Current URL, somehow. + # TODO: Current URL, somehow. pass def template(self, name, auto_escape=True, **values): @@ -254,5 +255,6 @@ class API: port = 0 bind_to = f"{address}:{port}" + import uvicorn - waitress.serve(app=self, listen=bind_to, **kwargs) + uvicorn.serve(app=self, listen=bind_to, **kwargs) diff --git a/responder/routes.py b/responder/routes.py new file mode 100644 index 0000000..35490d9 --- /dev/null +++ b/responder/routes.py @@ -0,0 +1,42 @@ +from parse import parse, search + + +class Route: + def __init__(self, route, endpoint): + self.route = route + self.endpoint = endpoint + + def __repr__(self): + return f"" + + def __eq__(self, other): + if hasattr(other, "route"): + # Being compared to other routes. + return self.route == other.route + else: + # Strings. + return self.does_match(other) + + @property + def has_parameters(self): + return all([("{" in self.route), ("}" in self.route)]) + + def does_match(self, s): + if s == self.route: + return True + + named = self.incoming_matches(s) + return bool(len(named)) + + def incoming_matches(self, s): + results = parse(self.route, s) + return results.named if results else {} + + def url(self, **params): + return self.route.format(**params) + + # def is_graphql, is_wsgi + + +r = Route(route="/2/{name}", endpoint=print) +print(r.does_match("/2/hello")) diff --git a/setup.py b/setup.py index b7ceaaf..465e717 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ required = [ "graphql-server-core>=1.1", "whitenoise", "jinja2", + "parse", ] diff --git a/test_responder.py b/test_responder.py index d48c7d8..de2918a 100644 --- a/test_responder.py +++ b/test_responder.py @@ -33,10 +33,6 @@ def schema(): return graphene.Schema(query=Query) -def test_api_fixture(api): - assert api - - def test_api_basic_route(api): @api.route("/") def home(req, resp): @@ -153,6 +149,11 @@ def test_graphql_schema_query_querying(api, schema): assert r.json() == {"data": {"hello": None}} -# GRAPHQL -def test_assert(): - assert True +def test_argumented_routing(api): + @api.route("/{name}") + def hello(req, resp, *, name): + print("yay") + resp.text = f"Hello, {name}." + + r = api.session().get("http://app/sean") + assert r.text == "Hello, sean."