Skip to content

Commit

Permalink
file upload
Browse files Browse the repository at this point in the history
  • Loading branch information
mission-liao committed Sep 1, 2014
1 parent 1bafdc3 commit 2c9e4ea
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 55 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ your API, the first option you find would be [Swagger-codegen](https://github.co
**pyswagger** is much easier to use (you don't need to prepare a scala environment) and tries hard to fully supports
[Swagger Spec](https://helloreverb.com/developers/swagger) in all aspects.

**TODO:** File uploading (the last piece finally)


- [Features](https://github.com/AntXlab/pyswagger/blob/master/README.md#features)
- [Quick Start](https://github.com/AntXlab/pyswagger/blob/master/README.md#quick-start)
Expand Down
15 changes: 13 additions & 2 deletions pyswagger/contrib/client/requests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import absolute_import
from ...core import BaseClient
from requests import Session, Request
import six


class Client(BaseClient):
Expand All @@ -19,14 +20,24 @@ def request(self, req_and_resp, opt={}):
req, resp = super(Client, self).request(req_and_resp, opt)

# apply request-related options before preparation.
req.prepare()
req.prepare(handle_files=False)

# prepare for uploaded files
file_obj = {}
for k, v in six.iteritems(req.files):
f = v.data or open(v.filename, 'rb')
if 'Content-Type' in v.header:
file_obj[k] = (v.filename, f, v.header['Content-Type'])
else:
file_obj[k] = (v.filename, f)

rq = Request(
method=req.method,
url=req.url,
params=req.query,
data=req.data,
headers=req.header
headers=req.header,
files=file_obj
)
rq = self.__s.prepare_request(rq)
rs = self.__s.send(rq)
Expand Down
2 changes: 1 addition & 1 deletion pyswagger/contrib/client/tornado.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def request(self, req_and_resp, opt={}):
"""
"""
req, resp = super(TornadoClient, self).request(req_and_resp, opt)
req.prepare()
req.prepare(handle_files=True)

url = url_concat(req.url, req.query)

Expand Down
156 changes: 109 additions & 47 deletions pyswagger/io.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
from __future__ import absolute_import
from .base import BaseObj
from .prim import PrimJSONEncoder
from uuid import uuid4
import six
import json
import io, codecs


class SwaggerRequest(object):
""" Request layer
"""

file_key = 'file_'

# options
opt_url_netloc = 'url_netloc'

def __init__(self, op, params={}, produces=None, consumes=None, authorizations=None):
"""
"""
self.__method = op.method
self.__p = dict(header={}, query={}, path={}, body={}, form={}, file_={})
self.__p = dict(header={}, query={}, path={}, body={}, form={}, File={})
self.__url = op._parent_.basePath + op.path
self.__prepared = False
self.__is_prepared = False
self.__header = {}

# TODO: this part can be resolved once by using scanner.
# let produces/consumes/authorizations in Operation override global ones.
Expand Down Expand Up @@ -49,48 +50,90 @@ def __init__(self, op, params={}, produces=None, consumes=None, authorizations=N
elif p.paramType == 'header':
converted = str(converted)

self.__p[p.paramType if p.type != 'File' else SwaggerRequest.file_key][p.name] = converted
self.__p[p.paramType if p.type != 'File' else 'File'][p.name] = converted

def _set_header(self):
""" prepare header section, reference implementation:
https://github.com/wordnik/swagger-js/blob/master/lib/swagger.js
"""
# update 'accept' header section
accepts = 'application/json'
content_type = 'application/json'

self.__header = self.__p['header']

if self.__p[SwaggerRequest.file_key]:
content_type = 'multipart/form-data'
elif self.__p['form']:
content_type = 'application/x-www-form-urlencoded'
elif self.__method == 'DELETE':
self.__p['body'] = {}

if content_type and self.__consumes and content_type not in self.__consumes:
content_type = self.__consumes[0]
if accepts and self.__produces and accepts not in self.__produces:
accepts = self.__produces[0]

if (content_type and self.__p['body']) or content_type == 'application/x-www-form-urlencoded':
self.__header['Content-Type'] = content_type
if accepts:
self.__header['Accept'] = accepts
self.__header.update({'Accept': accepts})

def _prepare_forms(self):
"""
"""
content_type = 'application/x-www-form-urlencoded'
if self.__consumes and content_type not in self.__consumes:
raise ValueError('unable to locate content-type: {0}'.format(content_type))

return content_type, six.moves.urllib.parse.urlencode(self.__p['form'])

def _encode(self, content_type, data):
def _prepare_body(self):
"""
"""
ret = ''
content_type = 'application/json'
if self.__consumes and content_type not in self.__consumes:
raise ValueError('unable to locate content-type: {0}'.format(content_type))

return content_type, json.dumps(
self.__p['body'], cls=PrimJSONEncoder)

def _prepare_files(self, encoding):
"""
"""
content_type = 'multipart/form-data'
if self.__consumes and content_type not in self.__consumes:
raise ValueError('unable to locate content-type: {0}'.format(content_type))

boundary = uuid4().hex
content_type += '; boundary={0}'
content_type = content_type.format(boundary)

if content_type == 'application/x-www-form-urlencoded':
ret = six.moves.urllib.parse.urlencode(data)
elif content_type == 'application/json':
ret = json.dumps(data, cls=PrimJSONEncoder)
elif content_type == 'multipart/form-data':
# it's wrong to pass any file related to here.
raise Exception('multipart/form-data encoding is not supported yet')
# init stream
body = io.BytesIO()
w = codecs.getwriter(encoding)

return ret
for k, v in six.iteritems(self.__p['form']):
body.write(six.b('--{0}\r\n'.format(boundary)))

w(body).write('Content-Disposition: form-data; name="{0}"'.format(k))
body.write(six.b('\r\n'))
body.write(six.b('\r\n'))

w(body).write(v)

body.write(six.b('\r\n'))

# begin of file section
for k, v in six.iteritems(self.__p['File']):
body.write(six.b('--{0}\r\n'.format(boundary)))

# header
w(body).write('Content-Disposition: form-data; name="{0}"; filename="{1}"'.format(k, v.filename))
body.write(six.b('\r\n'))
if 'Content-Type' in v.header:
w(body).write('Content-Type: {0}'.format(v.header['Content-Type']))
body.write(six.b('\r\n'))
if 'Content-Transfer-Encoding' in v.header:
w(body).write('Content-Transfer-Encoding: {0}'.format(v.header['Content-Transfer-Encoding']))
body.write(six.b('\r\n'))
body.write(six.b('\r\n'))


# body
if not v.data:
with open(v.filename, 'rb') as f:
body.write(f.read())
else:
body.write(v.data.read())

body.write(six.b('\r\n'))

# final boundary
body.write(six.b('--{0}--\r\n'.format(boundary)))

return content_type, body.getvalue()

def _patch(self, opt={}):
"""
Expand All @@ -104,32 +147,46 @@ def _patch(self, opt={}):

# if already prepared, prepare again to apply
# those patches.
if self.__prepared:
if self.__is_prepared:
self.prepare()

def prepare(self):
def prepare(self, handle_files=True, encoding='utf-8'):
""" make this request ready for any Client
"""

self.__prepared = True

self._set_header()
self.__is_prepared = True

# combine path parameters into url
self.__url = self.__url.format(**self.__p['path'])

# header parameters
self.__header.update(self.__p['header'])

# update data parameter
if self.__p[SwaggerRequest.file_key]:
self.__data = self.__p[SwaggerRequest.file_key]
elif 'Content-Type' in self.header:
self.__data = self._encode(
self.header['Content-Type'],
self.__p['form'] if self.__p['form'] else self.__p['body']
)
content_type = None
if self.__p['File']:
if handle_files:
content_type, self.__data = self._prepare_files(encoding)
else:
# client that can encode multipart/form-data, should
# access form-data via data property and file from file
# property.

# only form data can be carried along with files,
self.__data = self.__p['form']

elif self.__p['form']:
content_type, self.__data = self._prepare_forms()
elif self.__p['body']:
content_type, self.__data = self._prepare_body()
else:
self.__data = None

if content_type:
self.__header.update({'Content-Type': content_type})

return self


@property
def url(self):
Expand All @@ -156,6 +213,11 @@ def data(self):
""" data carried by this request """
return self.__data

@property
def files(self):
""" files of this request """
return self.__p['File']

@property
def _p(self):
""" for unittest/debug/internal purpose """
Expand Down
11 changes: 10 additions & 1 deletion pyswagger/prim.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,16 @@ class File(object):
"""
"""
def __init__(self, obj, val):
pass
"""
header:
Content-Type -> content-type
Content-Transfer-Encoding -> content-transder-encoding
filename -> name
file-like object or path -> data
"""
self.header = val.get('header', {})
self.data = val.get('data', None)
self.filename = val.get('filename', '')


class PrimJSONEncoder(json.JSONEncoder):
Expand Down
16 changes: 15 additions & 1 deletion pyswagger/tests/contrib/client/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import unittest
import httpretty
import json
import six


app = SwaggerApp._create_(get_test_data_folder(version='1.2', which='wordnik'))
Expand Down Expand Up @@ -156,5 +157,18 @@ def test_getPetById(self):
@httpretty.activate
def test_uploadFile(self):
""" Pet.uploadFile """
# TODO: implement File upload
httpretty.register_uri(httpretty.POST, 'http://petstore.swagger.wordnik.com/api/pet/uploadImage',
status=200)

resp = client.request(app.op['uploadFile'](
additionalMetadata='a test image', file=dict(data=six.StringIO('a test Content'), filename='test.txt')))

self.assertEqual(resp.status, 200)

body = httpretty.last_request().body.decode()
self.assertTrue(body.find('additionalMetadata') != -1)
self.assertTrue(body.find('a test image') != -1)
self.assertTrue(body.find('file') != -1)
self.assertTrue(body.find('a test Content') != -1)
self.assertTrue(body.find('filename="test.txt"') != -1)

Loading

0 comments on commit 2c9e4ea

Please sign in to comment.