Skip to content
This repository has been archived by the owner on Feb 25, 2022. It is now read-only.

Commit

Permalink
Add form data codecs (#430)
Browse files Browse the repository at this point in the history
* add .pytest_cache to .gitignore
* add URLEncodedCodec and MultiPartCodec
  • Loading branch information
Bogdanp authored and tomchristie committed Apr 19, 2018
1 parent 3a8e199 commit df5bc01
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -3,6 +3,7 @@
.coverage
.git
.mypy_cache
.pytest_cache
__pycache__
/*.egg-info
/htmlcov
Expand Down
3 changes: 2 additions & 1 deletion apistar/codecs/__init__.py
@@ -1,11 +1,12 @@
from apistar.codecs.base import BaseCodec
from apistar.codecs.download import DownloadCodec
from apistar.codecs.formdata import MultiPartCodec, URLEncodedCodec
from apistar.codecs.jsondata import JSONCodec
from apistar.codecs.jsonschema import JSONSchemaCodec
from apistar.codecs.openapi import OpenAPICodec
from apistar.codecs.text import TextCodec

__all__ = [
'BaseCodec', 'JSONCodec', 'JSONSchemaCodec', 'OpenAPICodec', 'TextCodec',
'DownloadCodec'
'DownloadCodec', 'MultiPartCodec', 'URLEncodedCodec',
]
36 changes: 36 additions & 0 deletions apistar/codecs/formdata.py
@@ -0,0 +1,36 @@
from io import BytesIO
from itertools import chain

from werkzeug.datastructures import ImmutableMultiDict
from werkzeug.formparser import FormDataParser
from werkzeug.http import parse_options_header
from werkzeug.urls import url_decode

from apistar.codecs.base import BaseCodec


class MultiPartCodec(BaseCodec):
media_type = 'multipart/form-data'

def decode(self, bytestring, headers, **options):
try:
content_length = max(0, int(headers['content-length']))
except (KeyError, ValueError, TypeError):
content_length = None

try:
mime_type, mime_options = parse_options_header(headers['content-type'])
except KeyError:
mime_type, mime_options = '', {}

body_file = BytesIO(bytestring)
parser = FormDataParser()
stream, form, files = parser.parse(body_file, mime_type, content_length, mime_options)
return ImmutableMultiDict(chain(form.items(), files.items()))


class URLEncodedCodec(BaseCodec):
media_type = 'application/x-www-form-urlencoded'

def decode(self, bytestring, **options):
return url_decode(bytestring, cls=ImmutableMultiDict)
6 changes: 5 additions & 1 deletion apistar/server/validation.py
Expand Up @@ -13,7 +13,11 @@

class RequestDataComponent(Component):
def __init__(self):
self.codecs = [codecs.JSONCodec()]
self.codecs = [
codecs.JSONCodec(),
codecs.URLEncodedCodec(),
codecs.MultiPartCodec(),
]

def can_handle_parameter(self, parameter: inspect.Parameter):
return parameter.annotation is http.RequestData
Expand Down
56 changes: 47 additions & 9 deletions tests/test_http.py
@@ -1,4 +1,5 @@
import pytest
from pytest import param

from apistar import Route, http, test
from apistar.server.app import App, ASyncApp
Expand Down Expand Up @@ -75,6 +76,16 @@ def get_request_data(data: http.RequestData):
return {'data': data}


def get_multipart_request_data(data: http.RequestData):
files = {
name: f if isinstance(f, str) else {
'filename': f.filename,
'content': f.read().decode('utf-8'),
} for name, f in data.items()
}
return {'data': files}


def return_string(data: http.RequestData) -> str:
return '<html><body>example content</body></html>'

Expand Down Expand Up @@ -107,6 +118,7 @@ def return_response(data: http.RequestData) -> http.Response:
Route('/path_params/{example}/', 'GET', get_path_params),
Route('/full_path_params/{+example}', 'GET', get_path_params, name='full_path_params'),
Route('/request_data/', 'POST', get_request_data),
Route('/multipart_request_data/', 'POST', get_multipart_request_data),
Route('/return_string/', 'GET', return_string),
Route('/return_data/', 'GET', return_data),
Route('/return_response/', 'GET', return_response),
Expand Down Expand Up @@ -288,15 +300,41 @@ def test_full_path_params(client):
assert response.json() == {'params': {'example': 'abc/def/'}}


def test_request_data(client):
response = client.post('/request_data/', json={'abc': 123})
assert response.json() == {'data': {'abc': 123}}
response = client.post('/request_data/')
assert response.json() == {'data': None}
response = client.post('/request_data/', data=b'...', headers={'content-type': 'unknown'})
assert response.status_code == 415
response = client.post('/request_data/', data=b'...', headers={'content-type': 'application/json'})
assert response.status_code == 400
@pytest.mark.parametrize('request_params,response_status,response_json', [
# JSON
param({'json': {'abc': 123}}, 200, {'data': {'abc': 123}}, id='valid json body'),
param({}, 200, {'data': None}, id='empty json body'),
# Urlencoding
param({'data': {'abc': 123}}, 200, {'data': {'abc': '123'}}, id='valid urlencoded body'),
param(
{'headers': {'content-type': 'application/x-www-form-urlencoded'}}, 200, {'data': None},
id='empty urlencoded body',
),
# Misc
param({'data': b'...', 'headers': {'content-type': 'unknown'}}, 415, None, id='unknown body type'),
param({'data': b'...', 'headers': {'content-type': 'application/json'}}, 400, None, id='json parse failure'),
])
def test_request_data(request_params, response_status, response_json, client):
response = client.post('/request_data/', **request_params)
assert response.status_code == response_status
if response_json is not None:
assert response.json() == response_json


def test_multipart_request_data(client):
response = client.post('/multipart_request_data/', files={'a': ('b', '123')}, data={'b': '42'})
assert response.status_code == 200
assert response.json() == {
'data': {
'a': {
'filename': 'b',
'content': '123',
},
'b': '42',
}
}


def test_return_string(client):
Expand Down

0 comments on commit df5bc01

Please sign in to comment.