From ed40a5d85204e712ec66fbeecc01a38fe4629f5c Mon Sep 17 00:00:00 2001 From: Johannes Valbjorn <johannes.valbjorn@gmail.com> Date: Fri, 12 Jan 2018 11:21:44 +0100 Subject: [PATCH 1/6] fix casting of path_params --- sanicargs/__init__.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/sanicargs/__init__.py b/sanicargs/__init__.py index 20e6520..52a6fd9 100644 --- a/sanicargs/__init__.py +++ b/sanicargs/__init__.py @@ -8,10 +8,10 @@ from logging import getLogger -logger = getLogger('sonicargs') +__logger = getLogger('sonicargs') -def parse_datetime(str): +def __parse_datetime(str): # attempt full date time, but tolerate just a date try: return datetime.datetime.strptime(str, '%Y-%m-%dT%H:%M:%S') @@ -19,14 +19,14 @@ def parse_datetime(str): pass return datetime.datetime.strptime(str, '%Y-%m-%d') -def parse_date(str): +def __parse_date(str): return datetime.datetime.strptime(str, '%Y-%m-%d').date() -type_deserializers = { +__type_deserializers = { int: int, str: str, - datetime.datetime: parse_datetime, - datetime.date: parse_date, + datetime.datetime: __parse_datetime, + datetime.date: __parse_date, List[str]: lambda s: s.split(',') } @@ -54,12 +54,16 @@ async def inner(request, *old_args, **route_parameters): name = None try: for name, arg_type, default in parameters: + raw_value = request.args.get(name, None) + # provided in route if name in route_parameters or name=="request": - continue + if name=="request": + continue + raw_value = route_parameters[name] # no value - if name not in request.args: + elif name not in request.args: if default != inspect._empty: # TODO clone? kwargs[name] = default @@ -67,13 +71,10 @@ async def inner(request, *old_args, **route_parameters): else: raise KeyError("Missing required argument %s" % name) - raw_value = request.args[name][0] - parsed_value = type_deserializers[arg_type](raw_value) + parsed_value = __type_deserializers[arg_type](raw_value) kwargs[name] = parsed_value - - kwargs.update(route_parameters) except Exception as err: - logger.warning({ + __logger.warning({ "message": "Request args not validated", "stacktrace": str(err) }) From 1fcf91c93993734941e69f8287cca0bc07fa05cf Mon Sep 17 00:00:00 2001 From: Johannes Valbjorn <johannes.valbjorn@gmail.com> Date: Fri, 12 Jan 2018 11:22:47 +0100 Subject: [PATCH 2/6] add tests for path-params, multiple params and optional params --- tests/test_sanicargs.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/test_sanicargs.py b/tests/test_sanicargs.py index a2814fc..bdb8553 100644 --- a/tests/test_sanicargs.py +++ b/tests/test_sanicargs.py @@ -54,6 +54,16 @@ async def test_all( 'e': e }) + @app.route("/optional", methods=['GET']) + @parse_query_args + async def test_optional(request, test: str = 'helloworld'): + return response.json({'test': test}) + + @app.route("/with/<path_param>/path_params", methods=['GET']) + @parse_query_args + async def test_optional(request, path_param: int, test: str, test_2: int=35): + return response.json({'path_param': path_param, 'test': test, 'test_2': test_2}) + yield app @pytest.fixture @@ -65,6 +75,7 @@ def test_cli(loop, app, test_client): # Tests # ######### + async def test_parse_int_success(test_cli): resp = await test_cli.get('/int?test=10') assert resp.status == 200 @@ -132,6 +143,7 @@ async def test_parse_list_also_works_with_singular(test_cli): 'not a datetime' ]} + async def test_all_at_once(test_cli): resp = await test_cli.get('/all?a=10&b=test&c=2017-10-10T10:10:10&d=2017-10-10&e=a,b,c,d,e') assert resp.status == 200 @@ -144,4 +156,23 @@ async def test_all_at_once(test_cli): e=[ 'a', 'b', 'c', 'd', 'e' ] - ) \ No newline at end of file + ) + + +async def test_optional(test_cli): + resp = await test_cli.get('/optional') + assert resp.status == 200 + resp_json = await resp.json() + assert resp_json == {'test': 'helloworld'} + + +async def test_mandatory(test_cli): + resp = await test_cli.get('/str') + assert resp.status == 400 + + +async def test_with_path_params(test_cli): + resp = await test_cli.get('/with/123/path_params?test=hello') + assert resp.status == 200 + resp_json = await resp.json() + assert resp_json == {'path_param': 123, 'test': 'hello', 'test_2': 35} From b04cee7d294f58eeba934dc1623ca9f4f7e91750 Mon Sep 17 00:00:00 2001 From: Johannes Valbjorn <johannes.valbjorn@gmail.com> Date: Fri, 12 Jan 2018 12:54:42 +0100 Subject: [PATCH 3/6] add simple example --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++------ examples/simple.py | 15 +++++++++++++ tests/__init__.py | 1 + 3 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 examples/simple.py diff --git a/README.md b/README.md index 3023486..b6292a2 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,56 @@ [](https://travis-ci.org/trustpilot/python-sanicargs) [](https://pypi.python.org/pypi/sanicargs) [](https://pypi.python.org/pypi/sanicargs) # Sanicargs -Parses query args in sanic using type annotations +Parses query args in [Sanic](https://github.com/channelcat/sanic) using type annotations. + +## Install +Install with pip +``` +$ pip install sanicargs +``` ## Usage -Use with [Sanic framework](https://github.com/channelcat/sanic) +Use the `parse_query_args` decorator with [Sanic](https://github.com/channelcat/sanic)'s routes or blueprints like in the [example](https://github.com/trustpilot/python-sanicargs/tree/master/examples/simple.py) below: + +```python +import datetime +from sanic import Sanic, response +from sanicargs import parse_query_args + +app = Sanic("test_sanic_app") + +@app.route("/me/<id>/birthdate", methods=['GET']) +@parse_query_args +async def test_datetime(request, id: str, birthdate: datetime.datetime): + return response.json({ + 'id': id, + 'birthdate': birthdate.isoformat() + }) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080, access_log=False, debug=False) +``` + +Test it running with +```bash +$ curl 'http://0.0.0.0:8080/me/123/birthdate?birthdate=2017-10-30' ``` - @app.route("/datetime", methods=['GET']) - @parse_query_args - async def test_datetime(request, test: datetime.datetime): - return response.json({'test': test.isoformat()}) -``` \ No newline at end of file + +### Fields + +* **str** : `ex: ?message=hello world` +* **int** : `ex: ?age=100` +* **datetime.datetime** : `ex: ?currentdate=2017-10-30T10:10:30 or 2017-10-30` +* **datetime.date** : `ex: ?birthdate=2017-10-30` +* **List[str]** : `ex: ?words=you,me,them,we` + +### Important notice about decorators + +The sequence of decorators is, as usual, important in Python. + +You need to apply the `parse_query_args` decorator as the first one executed which means closest to the `def`. + +### `request` is mandatory! + +You should always have request as the first argument in your function in order to use `parse_query_args` \ No newline at end of file diff --git a/examples/simple.py b/examples/simple.py new file mode 100644 index 0000000..711269c --- /dev/null +++ b/examples/simple.py @@ -0,0 +1,15 @@ +import datetime +from sanic import Sanic, response +from sanicargs import parse_query_args + +app = Sanic("test_sanic_app") + +@app.route("/me/<id>/birthdate", methods=['GET']) +@parse_query_args +async def test_datetime(request, id: str, birthdate: datetime.datetime): + return response.json({ + 'id': id, + 'birthdate': birthdate.isoformat() + }) +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8080, access_log=False, debug=False) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..8b13789 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ + From 966ff7a28b98981f60e14b8eb5c07149bd1f69db Mon Sep 17 00:00:00 2001 From: Johannes Valbjorn <johannes.valbjorn@gmail.com> Date: Fri, 12 Jan 2018 12:58:35 +0100 Subject: [PATCH 4/6] fix linting --- .travis.yml | 3 ++- examples/simple.py | 2 +- tests/__init__.py | 1 - tests/test_sanicargs.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 76d6ba0..592f08a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,8 @@ install: - pip install pypandoc - pip install .[test] script: -- py.test +- prospector -M +- pytest branches: only: master after_success: diff --git a/examples/simple.py b/examples/simple.py index 711269c..fc43d49 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -12,4 +12,4 @@ async def test_datetime(request, id: str, birthdate: datetime.datetime): 'birthdate': birthdate.isoformat() }) if __name__ == "__main__": - app.run(host="0.0.0.0", port=8080, access_log=False, debug=False) \ No newline at end of file + app.run(host="0.0.0.0", port=8080, access_log=False, debug=False) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index 8b13789..e69de29 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +0,0 @@ - diff --git a/tests/test_sanicargs.py b/tests/test_sanicargs.py index bdb8553..abc176d 100644 --- a/tests/test_sanicargs.py +++ b/tests/test_sanicargs.py @@ -61,7 +61,7 @@ async def test_optional(request, test: str = 'helloworld'): @app.route("/with/<path_param>/path_params", methods=['GET']) @parse_query_args - async def test_optional(request, path_param: int, test: str, test_2: int=35): + async def test_path_params(request, path_param: int, test: str, test_2: int=35): return response.json({'path_param': path_param, 'test': test, 'test_2': test_2}) yield app From 8a5e88cbd3a61081614a806d0e9e984ef5078725 Mon Sep 17 00:00:00 2001 From: Johannes Valbjorn <johannes.valbjorn@gmail.com> Date: Fri, 12 Jan 2018 13:01:59 +0100 Subject: [PATCH 5/6] bump version, fixups in README --- HISTORY.rst | 8 +++++++- README.md | 2 +- sanicargs/__version__.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 031483b..7fb62b1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,4 +4,10 @@ History 0.0.1 (2017-08-09) ------------------ -* git init \ No newline at end of file +* git init + +1.0.0(2017-08-09) +------------------ + +* added test suite +* added path_param type casting using \ No newline at end of file diff --git a/README.md b/README.md index b6292a2..b4b2657 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ $ pip install sanicargs ## Usage -Use the `parse_query_args` decorator with [Sanic](https://github.com/channelcat/sanic)'s routes or blueprints like in the [example](https://github.com/trustpilot/python-sanicargs/tree/master/examples/simple.py) below: +Use the `parse_query_args` decorator to parse query args and type cast query args and path params with [Sanic](https://github.com/channelcat/sanic)'s routes or blueprints like in the [example](https://github.com/trustpilot/python-sanicargs/tree/master/examples/simple.py) below: ```python import datetime diff --git a/sanicargs/__version__.py b/sanicargs/__version__.py index 8dbfdad..75977e6 100644 --- a/sanicargs/__version__.py +++ b/sanicargs/__version__.py @@ -1 +1 @@ -__version__ = '0.0.3' \ No newline at end of file +__version__ = '1.0.0' \ No newline at end of file From 0a6368f9737c20cea25a96a83ff2079a7ccd2894 Mon Sep 17 00:00:00 2001 From: Johannes Valbjorn <johannes.valbjorn@gmail.com> Date: Fri, 12 Jan 2018 13:13:26 +0100 Subject: [PATCH 6/6] review fixups --- HISTORY.rst | 6 +++--- sanicargs/__init__.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 7fb62b1..e270965 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,13 +1,13 @@ History ======= -0.0.1 (2017-08-09) +0.0.1 (2017-01-09) ------------------ * git init -1.0.0(2017-08-09) +1.0.0(2017-01-12) ------------------ * added test suite -* added path_param type casting using \ No newline at end of file +* added path_param type casting \ No newline at end of file diff --git a/sanicargs/__init__.py b/sanicargs/__init__.py index 52a6fd9..4012a67 100644 --- a/sanicargs/__init__.py +++ b/sanicargs/__init__.py @@ -8,7 +8,7 @@ from logging import getLogger -__logger = getLogger('sonicargs') +__logger = getLogger('sanicargs') def __parse_datetime(str):