Skip to content

Commit

Permalink
Merge pull request #1 from trustpilot/fix-path-params
Browse files Browse the repository at this point in the history
Fix path params
  • Loading branch information
sloev authored Jan 12, 2018
2 parents 225a0ef + 0a6368f commit 73fe31e
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 25 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ install:
- pip install pypandoc
- pip install .[test]
script:
- py.test
- prospector -M
- pytest
branches:
only: master
after_success:
Expand Down
10 changes: 8 additions & 2 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
History
=======

0.0.1 (2017-08-09)
0.0.1 (2017-01-09)
------------------

* git init
* git init

1.0.0(2017-01-12)
------------------

* added test suite
* added path_param type casting
56 changes: 49 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/<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()})
```

### 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`
15 changes: 15 additions & 0 deletions examples/simple.py
Original file line number Diff line number Diff line change
@@ -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)
27 changes: 14 additions & 13 deletions sanicargs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@

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')
except:
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(',')
}

Expand Down Expand Up @@ -54,26 +54,27 @@ 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
continue
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)
})
Expand Down
2 changes: 1 addition & 1 deletion sanicargs/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.0.3'
__version__ = '1.0.0'
33 changes: 32 additions & 1 deletion tests/test_sanicargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -144,4 +156,23 @@ async def test_all_at_once(test_cli):
e=[
'a', 'b', 'c', 'd', 'e'
]
)
)


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}

0 comments on commit 73fe31e

Please sign in to comment.