├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── swagger ├── __init__.py ├── exceptions.py └── swagger.py └── tests ├── __init__.py └── test_swagger.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.2" 5 | - "3.3" 6 | - "3.4" 7 | # command to install dependencies 8 | install: 9 | - pip install -r requirements.txt 10 | - pip install -r requirements-dev.txt 11 | # command to run tests 12 | script: nosetests 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Jason Walsh 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyswagger [![Build Status](https://img.shields.io/travis/rightlag/pyswagger/master.svg?style=flat-square)](https://travis-ci.org/rightlag/pyswagger) [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square)](https://gitter.im/rightlag/pyswagger) 2 | 3 | pyswagger 0.2.0 4 | 5 | Released: 7-Jan-2016 6 | 7 | # Release Notes 8 | 9 | - **Release 0.2.0** 10 | - Support for both token-based and HTTP basic authentication (e.g. `apiKey`, `basic`) 11 | - Support for Swagger schema specifications to be read from hosted sites instead of reading them from local device 12 | - Scheme is automatically assigned if not passed as an argument when issuing requests (e.g. `http`, `https`, `ws`, `wss`) 13 | - Minor bug fixes 14 | 15 | - **Release 0.1.0** 16 | - Reads Swagger schema specifications 17 | - Creates a `client` object used to instantiate requests to paths defined in the schema 18 | - Supports `apiKey` authentication 19 | - Supports common request methods (e.g. `GET`, `POST`, `PUT`, and `DELETE`) 20 | 21 | - **Roadmap** 22 | - `$ref` support 23 | - Automatically determine MIME type for content-negotiation if not specified when issuing requests 24 | - ~~Support for OAuth~~ 25 | 26 | # Introduction 27 | 28 | pyswagger is a Python toolkit that reads any JSON formatted [Swagger](http://swagger.io/) (Open API) schema and generates methods for the [operations](http://swagger.io/specification/#operationObject) defined in the schema. 29 | 30 | # Quickstart 31 | 32 | To use the pyswagger client, import the `Swagger` class from the `swagger` module. The following example uses the [Swagger Petstore](http://petstore.swagger.io/) API. 33 | 34 | ```python 35 | >>> from swagger import Swagger 36 | >>> client = Swagger.load('http://petstore.swagger.io/v2/swagger.json') 37 | >>> res = client.get('/pet/findByStatus', status='sold') 38 | >>> print res.json() 39 | [{u'category': {u'id': 1, u'name': u'Dogs'}, u'status': u'sold', u'name': u'Dog 2', u'tags': [{u'id': 1, u'name': u'tag2'}, {u'id': 2, u'name': u'tag3'}], u'photoUrls': [u'url1', u'url2'], u'id': 5}] 40 | ``` 41 | 42 | This returns a list of `Pet` objects whose `status` attribute is assigned `sold`. 43 | 44 | The `load()` method requires a URL to where the schema exists. The schema **must** be JSON formatted. In this example, the petstore schema is being loaded via a HTTP GET request and is then deserialized. 45 | 46 | The `status` keyword argument is located within the list of parameters of the `/pet/findByStatus` path in the `petstore.json` schema. 47 | 48 | # Query parameters 49 | 50 | Endpoints that accept optional or required query parameters can be passed as keyword arguments to the method call. 51 | 52 | # Endpoints containing IDs 53 | 54 | For endpoints that contain IDs (e.g. `/pet/2`), pyswagger uses string interpolation to match the ID with the respective keyword argument. The following example simulates a `GET` request that will return a pet with ID `2`: 55 | 56 | ```python 57 | from swagger import Swagger 58 | >>> client = Swagger.load('http://petstore.swagger.io/v2/swagger.json') 59 | >>> res = client.get('/pet/{petId}', petId=2) 60 | >>> print res.json() 61 | {u'category': {u'id': 2, u'name': u'Cats'}, u'status': u'available', u'name': u'Cat 2', u'tags': [{u'id': 1, u'name': u'tag2'}, {u'id': 2, u'name': u'tag3'}], u'photoUrls': [u'url1', u'url2'], u'id': 2} 62 | ``` 63 | 64 | The `{petId}` placeholder is matched in the endpoint string and is replaced with the value of the `petId` keyword argument. 65 | 66 | # Requests containing a payload 67 | 68 | For requests that require a request payload, the `body` keyword argument can be passed as an argument to the method. The value of the `body` argument *should* be [serialized](https://en.wikipedia.org/wiki/Serialization). The following example simulates a `POST` request that will create a new pet: 69 | 70 | ```python 71 | >>> import json 72 | >>> from swagger import Swagger 73 | >>> data = { 74 | ... 'id': 0, 75 | ... 'category': { 76 | ... 'id': 0, 77 | ... 'name': 'string', 78 | ... }, 79 | ... 'name': 'doggie', 80 | ... 'photoUrls': [ 81 | ... 'string', 82 | ... ], 83 | ... 'tags': [ 84 | ... { 85 | ... 'id': 0, 86 | ... 'name': 'string', 87 | ... } 88 | ... ], 89 | ... 'status': 'available', 90 | ... } 91 | >>> data = json.dumps(data) 92 | >>> client = Swagger.load('http://petstore.swagger.io/v2/swagger.json') 93 | >>> res = client.post('/pet', body=data, auth='special-key') 94 | >>> print res.status_code, res.reason 95 | 200 OK 96 | ``` 97 | 98 | **Note:** Some endpoints do not return a response body. Therefore, invoking the `.json()` method on the response object will raise an exception. 99 | 100 | The example above also includes the `auth` keyword argument which is explained in further detail in the next section. 101 | 102 | # Authenticated endpoints 103 | 104 | Authentication is sometimes required to access some or all endpoints of a web API. Since pyswagger is a client-side toolkit, it does not support authentication schemes such as [OAuth](https://en.wikipedia.org/wiki/OAuth). However, if the endpoint requires an access token to make a request, then the `auth` keyword argument can be supplied. 105 | 106 | ## Using the `auth` keyword argument 107 | 108 | Swagger uses [Security Definitions](http://swagger.io/specification/#securityDefinitionsObject) to define security schemes available to be used in the specification. For [token-based authentication](https://scotch.io/tutorials/the-ins-and-outs-of-token-based-authentication), The `in` field states the location of the API key which is either the `query` or the `header`. For [HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication), the `in` keyword is *not* defined. 109 | 110 | If a token-based authentication security definition exists in the schema, pyswagger inspects the value of the `in` field and automatically assigns it as a request header or a query parameter. Therefore, when using the `auth` keyword, it is not required to specify the location of the API key. 111 | 112 | **Token authentication** 113 | 114 | To use token authentication, the `auth` keyword argument *should* be of type `str`. 115 | 116 | ```python 117 | >>> from swagger import Swagger 118 | >>> client = Swagger.load('http://petstore.swagger.io/v2/swagger.json') 119 | >>> res = client.get('/pet/{petId}', petId=2, auth='special-token') 120 | ``` 121 | 122 | **HTTP basic authentication** 123 | 124 | To use HTTP basic authentication, the `auth` keyword argument *should* be of type `tuple`. 125 | 126 | ```python 127 | >>> from swagger import Swagger 128 | >>> client = Swagger.load('http://petstore.swagger.io/v2/swagger.json') 129 | >>> res = client.get('/pet/{petId}', petId=2, auth=('username', 'password')) 130 | ``` 131 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | nose==1.3.7 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.9.1 2 | wheel==0.24.0 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from codecs import open 4 | from setuptools import setup, find_packages 5 | 6 | 7 | here = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | with open(os.path.join(here, 'requirements.txt'), encoding='utf-8') as fp: 10 | requirements = fp.read().splitlines() 11 | 12 | setup( 13 | name='pyswagger', 14 | version='0.1.0', 15 | description=( 16 | 'A Python client that reads a JSON formatted Swagger schema ' + 17 | 'generates methods to interface directly with the HTTP API' 18 | ), 19 | url='https://cto-github.cisco.com/rightlag/pyswagger', 20 | author='Jason Walsh', 21 | author_email='jaswalsh@cisco.com', 22 | license='MIT', 23 | classifiers=[ 24 | 'Development Status :: 3 - Alpha', 25 | ], 26 | keywords='swagger client sdk', 27 | packages=find_packages(exclude=['test*']), 28 | install_requires=requirements 29 | ) 30 | -------------------------------------------------------------------------------- /swagger/__init__.py: -------------------------------------------------------------------------------- 1 | from .swagger import Swagger 2 | 3 | __author__ = 'Jason Walsh' 4 | __version__ = '0.1.0' 5 | -------------------------------------------------------------------------------- /swagger/exceptions.py: -------------------------------------------------------------------------------- 1 | class SwaggerServerError(Exception): 2 | def __init__(self, status_code, reason): 3 | self.status_code = status_code 4 | self.reason = reason 5 | 6 | def __str__(self): 7 | return '{} {}'.format(self.status_code, self.reason) 8 | 9 | 10 | class InvalidPathError(Exception): 11 | def __init__(self, path): 12 | self.path = path 13 | 14 | def __str__(self): 15 | return 'Got unexpected path \'{}\''.format(self.path) 16 | 17 | 18 | class InvalidOperationError(KeyError): 19 | def __init__(self, operation): 20 | self.operation = operation 21 | 22 | def __str__(self): 23 | return 'Invalid operation \'{}\''.format(self.operation) 24 | -------------------------------------------------------------------------------- /swagger/swagger.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | try: 5 | # py3.x 6 | from urllib.parse import urlparse 7 | except ImportError: 8 | # py2.x 9 | from urlparse import urlparse 10 | from .exceptions import SwaggerServerError 11 | from .exceptions import InvalidPathError 12 | from .exceptions import InvalidOperationError 13 | 14 | 15 | class Swagger(object): 16 | ResponseError = SwaggerServerError 17 | DefaultFormat = 'application/json' 18 | DefaultOperations = ('get', 'put', 'post', 'delete', 'options', 'head', 19 | 'patch',) 20 | 21 | def __init__(self): 22 | self._baseUri = None 23 | self._timeout = 10.0 24 | self._session = requests.Session() 25 | 26 | @property 27 | def baseUri(self): 28 | return self._baseUri 29 | 30 | @baseUri.setter 31 | def baseUri(self, baseUri): 32 | if hasattr(self, 'basePath'): 33 | baseUri += self.basePath 34 | self._baseUri = baseUri 35 | 36 | @property 37 | def timeout(self): 38 | return self._timeout 39 | 40 | @timeout.setter 41 | def timeout(self, timeout): 42 | self._timeout = float(timeout) 43 | 44 | @property 45 | def auth(self): 46 | return self._session.auth 47 | 48 | @auth.setter 49 | def auth(self, auth): 50 | for _, definition in list(self.securityDefinitions.items()): 51 | if definition['type'] == 'apiKey': 52 | parameterIn = definition['in'] 53 | if parameterIn == 'header': 54 | # Assign the `apiKey` header used for token 55 | # authentication. 56 | self._session.headers[definition['name']] = auth 57 | elif definition['type'] == 'basic': 58 | # Assign the `Authorization` header used for Basic 59 | # authentication. 60 | self._session.auth = auth 61 | 62 | @property 63 | def headers(self): 64 | return self._session.headers 65 | 66 | @headers.setter 67 | def headers(self, schema): 68 | """Set the `Accept` and `Content-Type` headers for the request. 69 | 70 | :type schema: dict 71 | :param schema: The schema definition object 72 | """ 73 | if 'consumes' in schema: 74 | self._session.headers['Content-Type'] = schema['consumes'] 75 | if 'produces' in schema: 76 | self._session.headers['Accept'] = schema['produces'] 77 | 78 | @staticmethod 79 | def load(url): 80 | """Load Swagger schema file and return a new client instance. 81 | 82 | :type url: str 83 | :param url: The URL to the Swagger schema 84 | """ 85 | response = requests.get(url) 86 | if response.status_code not in list(range(200, 300)): 87 | response.raise_for_status() 88 | schema = response.json() 89 | instance = Swagger() 90 | # Assign the Swagger version to the client instance. 91 | instance.Version = schema.pop('swagger') 92 | for field, obj in list(schema.items()): 93 | setattr(instance, field, obj) 94 | # Assign the `_baseUri` property of the client. The request 95 | # protocol is assigned when issuing the request. 96 | url = urlparse(url) 97 | instance._baseUri = '{scheme}://{host}{basePath}'.format( 98 | scheme=url.scheme, 99 | host=instance.host, 100 | basePath=( 101 | instance.basePath if hasattr(instance, 'basePath') else '' 102 | ) 103 | ) 104 | # Assign the global headers of the schema. Headers can be 105 | # overridden in the operation callback method. 106 | instance.headers = schema 107 | return instance 108 | 109 | def __getattr__(self, fn): 110 | def callback(self, *args, **kwargs): 111 | """Callback method for issuing requests via the operations 112 | defined in the paths""" 113 | try: 114 | # If the `path` argument is not passed, raise a 115 | # `ValueError` exception. 116 | path = args[0] 117 | except IndexError: 118 | raise ValueError('Path argument not provided') 119 | if path not in self.paths: 120 | # If the `path` does not exist, raise `InvalidPathError` 121 | # exception. 122 | raise InvalidPathError(path) 123 | operation = self.paths[path][fn] 124 | if 'security' in operation: 125 | if 'auth' in kwargs: 126 | auth = kwargs.pop('auth') 127 | self.auth = auth 128 | # Use string interpolation to replace placeholders with 129 | # keyword arguments. 130 | path = path.format(**kwargs) 131 | url = '{baseUri}{path}'.format(baseUri=self._baseUri, path=path) 132 | # If the `body` keyword argument exists, remove it from the 133 | # keyword argument dictionary and pass it as an argument 134 | # when issuing the request. 135 | body = kwargs.pop('body', {}) 136 | # Override the default headers defined in the root schema. 137 | produces = kwargs.pop('produces', self.DefaultFormat) 138 | if produces not in operation['produces']: 139 | # The MIME type does not exist in the 140 | # content-negotiation header. 141 | return 142 | headers = { 143 | 'Accept': produces, 144 | } 145 | try: 146 | response = self._session.request( 147 | fn, url, params=kwargs, data=body, timeout=self._timeout, 148 | headers=headers 149 | ) 150 | except requests.exceptions.SSLError: 151 | # If the request fails via a `SSLError`, re-instantiate 152 | # the request with the `verify` argument assigned to 153 | # `False`. 154 | response = self._session.request( 155 | fn, url, params=kwargs, data=body, verify=False, 156 | timeout=self._timeout, headers=headers 157 | ) 158 | if response.status_code not in list(range(200, 300)): 159 | # If the response status code is a non-2XX code, raise a 160 | # `ResponseError`. The `reason` variable attempts to 161 | # retrieve the `description` key if it is provided in 162 | # the `response` object. Otherwise, the default response 163 | # `reason` is used. 164 | try: 165 | reason = ( 166 | operation['responses'][str(response.status_code)].get( 167 | 'description', response.reason 168 | ) 169 | ) 170 | except KeyError: 171 | # Use the default `status_code` and `reason` 172 | # returned from the response. 173 | reason = response.reason 174 | raise self.ResponseError(response.status_code, reason) 175 | return response 176 | if fn not in self.DefaultOperations: 177 | # If the method does not exist in the `DefaultOperations`, 178 | # raise an `InvalidOperationError` exception. 179 | raise InvalidOperationError(fn) 180 | return callback.__get__(self) 181 | 182 | def __repr__(self): 183 | return '<{}: {}>'.format(self.__class__.__name__, self._baseUri) 184 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rightlag/pyswagger/cade7d629fd11782131e4c3d8e5de03060df2f4d/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_swagger.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | from swagger import Swagger 5 | 6 | 7 | class SwaggerTestCast(unittest.TestCase): 8 | def setUp(self): 9 | # Load the schema to create the client object. 10 | self.client = Swagger.load( 11 | 'http://petstore.swagger.io/v2/swagger.json' 12 | ) 13 | self.data = { 14 | 'id': 0, 15 | 'category': { 16 | 'id': 0, 17 | 'name': 'string', 18 | }, 19 | 'name': 'doggie', 20 | 'photoUrls': [ 21 | 'string', 22 | ], 23 | 'tags': [ 24 | { 25 | 'id': 0, 26 | 'name': 'string', 27 | }, 28 | ], 29 | 'status': 'available', 30 | } 31 | 32 | @property 33 | def pet(self): 34 | data = json.dumps(self.data) 35 | response = self.client.post('/pet', body=data, auth='special-key') 36 | return response.json() 37 | 38 | def test_swagger_version(self): 39 | """Assert Swagger version is '2.0'""" 40 | self.assertEqual(self.client.Version, '2.0') 41 | 42 | def test_create_pet_endpoint(self): 43 | data = json.dumps(self.data) 44 | expected_url = 'http://petstore.swagger.io/v2/pet' 45 | response = self.client.post('/pet', body=data, auth='special-key') 46 | self.assertEqual(response.url, expected_url) 47 | self.assertTrue(isinstance(response.json(), dict)) 48 | self.assertEqual(response.status_code, 200) 49 | 50 | def test_get_pet_by_id_endpoint(self): 51 | petId = self.pet['id'] 52 | response = self.client.get('/pet/{petId}', petId=petId) 53 | self.assertEqual(response.status_code, 200) 54 | 55 | def test_find_pets_by_status_endpoint(self): 56 | statuses = ('available', 'pending', 'sold',) 57 | for status in statuses: 58 | response = self.client.get('/pet/findByStatus', status=status) 59 | expected_url = ( 60 | 'http://petstore.swagger.io/v2/pet/findByStatus?status={}' 61 | ).format(status) 62 | self.assertEqual(response.url, expected_url) 63 | self.assertEqual(response.status_code, 200) 64 | self.assertTrue(isinstance(response.json(), list)) 65 | 66 | def test_find_pet_by_id_endpoint(self): 67 | petId = self.pet['id'] 68 | response = self.client.get('/pet/{petId}', petId=petId) 69 | self.assertEqual(response.status_code, 200) 70 | self.assertTrue(isinstance(response.json(), dict)) 71 | 72 | def test_pet_update_endpoint(self): 73 | petId = self.pet['id'] 74 | response = self.client.post( 75 | '/pet/{petId}', petId=petId, name='foo', status='bar', 76 | format='application/x-www-form-urlencoded' 77 | ) 78 | self.assertEqual(response.status_code, 200) 79 | 80 | def test_delete_pet_endpoint(self): 81 | petId = self.pet['id'] 82 | response = self.client.delete('/pet/{petId}', petId=petId, 83 | auth='special-key') 84 | self.assertEqual(response.status_code, 200) 85 | --------------------------------------------------------------------------------