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/HISTORY.rst b/HISTORY.rst index 031483b..e270965 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,7 +1,13 @@ History ======= -0.0.1 (2017-08-09) +0.0.1 (2017-01-09) ------------------ -* git init \ No newline at end of file +* git init + +1.0.0(2017-01-12) +------------------ + +* added test suite +* added path_param type casting \ No newline at end of file diff --git a/README.md b/README.md index 3023486..b4b2657 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,56 @@ [![Build Status](https://travis-ci.org/trustpilot/python-sanicargs.svg?branch=master)](https://travis-ci.org/trustpilot/python-sanicargs) [![Latest Version](https://img.shields.io/pypi/v/sanicargs.svg)](https://pypi.python.org/pypi/sanicargs) [![Python Support](https://img.shields.io/pypi/pyversions/sanicargs.svg)](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 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 +from sanic import Sanic, response +from sanicargs import parse_query_args + +app = Sanic("test_sanic_app") + +@app.route("/me//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..fc43d49 --- /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//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/sanicargs/__init__.py b/sanicargs/__init__.py index 20e6520..4012a67 100644 --- a/sanicargs/__init__.py +++ b/sanicargs/__init__.py @@ -8,10 +8,10 @@ from logging import getLogger -logger = getLogger('sonicargs') +__logger = getLogger('sanicargs') -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) }) 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 diff --git a/tests/test_sanicargs.py b/tests/test_sanicargs.py index a2814fc..abc176d 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_params", methods=['GET']) + @parse_query_args + 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 @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}