Skip to content

Commit

Permalink
openapi3 python package
Browse files Browse the repository at this point in the history
Hey William!

I really love your openapi3 package! I have done a couple of changes to
it according to your roadmap and would like to ask you to review please?

Do you think it would be possible to commit them and release a 0.0.2 on
pypi.org?

I am attaching all my changes to this mail. git-am(1) can apply them to
the repository.

Thank you so much!
Matthias

PS: I am on freenode (nick: mweckbecker) in #linode should you want to
discuss anything regarding openapi3. I'm happy to contribute much more.

From de617f3ef308710449c8b26f9146903fe9699411 Mon Sep 17 00:00:00 2001
From: Matthias Weckbecker <[email protected]>
Date: Wed, 5 Jun 2019 09:34:24 +0200
Subject: [PATCH 01/10] Make code pep8 compliant

Signed-off-by: Matthias Weckbecker <[email protected]>
  • Loading branch information
mweckbecker authored and Dorthu committed Jun 20, 2019
1 parent 2a885f9 commit 1ca675f
Show file tree
Hide file tree
Showing 14 changed files with 292 additions and 233 deletions.
2 changes: 2 additions & 0 deletions .pep8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pycodestyle]
ignore = E221,E501,E731
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ specs. For example, using `Linode's OpenAPI 3 Specification`_ for reference::
# the returned models is still of the correct type
type(new_linode) == type(linode) # True

HTTP basic authentication and HTTP digest authentication works like this::

# authenticate using a securityScheme defined in the spec's components.securtiySchemes
# Tuple with (username, password) as second argument
api.authenticate('basicAuth', ('username', 'password'))

Roadmap
-------

Expand Down
2 changes: 2 additions & 0 deletions openapi3/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from .openapi import OpenAPI


def main():
specfile = sys.argv[1]

Expand All @@ -22,5 +23,6 @@ def main():
else:
print("OK")


if __name__ == '__main__':
main()
24 changes: 13 additions & 11 deletions openapi3/components.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
from .object_base import ObjectBase


class Components(ObjectBase):
"""
A `Components Object`_ holds a reusable set of different aspects of the OAS
spec.
.. _Components Object: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#componentsObject
"""
__slots__ = ['schemas','responses','parameters','examples','requestBodies',
'headers','securitySchemes','links','callback']
__slots__ = ['schemas', 'responses', 'parameters', 'examples', 'headers',
'requestBodies', 'securitySchemes', 'links', 'callback']

def _parse_data(self):
"""
Implementation of :any:`ObjectBase._parse_data`
"""
self.schemas = self._get('schemas', ['Schema','Reference'], is_map=True)
self.responses = self._get('responses', ['Response','Reference'], is_map=True)
self.parameters = self._get('parameters', ['Parameter','Reference'], is_map=True)
self.examples = self._get('examples', ['Example','Reference'], is_map=True)
self.requestBodies = self._get('requestBody', ['RequestBody', 'Reference'], is_map=True)
#self.headers = self._get('headers', ['Header','Reference'], is_map=True)
self.securitySchemes = self._get('securitySchemes', ['SecurityScheme','Reference'], is_map=True)
#self.links = self._get('link', ['Link','Reference'], is_map=True)
#self.callbacks = self._get('callbacks', ['Callback','Reference'], is_map=True)
self.examples = self._get('examples', ['Example', 'Reference'], is_map=True)
self.parameters = self._get('parameters', ['Parameter', 'Reference'], is_map=True)
self.requestBodies = self._get('requestBody', ['RequestBody', 'Reference'], is_map=True)
self.responses = self._get('responses', ['Response', 'Reference'], is_map=True)
self.schemas = self._get('schemas', ['Schema', 'Reference'], is_map=True)
self.securitySchemes = self._get('securitySchemes', ['SecurityScheme', 'Reference'], is_map=True)
# self.headers = self._get('headers', ['Header', 'Reference'], is_map=True)
# self.links = self._get('link', ['Link', 'Reference'], is_map=True)
# self.callbacks = self._get('callbacks', ['Callback', 'Reference'], is_map=True)
5 changes: 3 additions & 2 deletions openapi3/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ class SpecError(ValueError):
object in the spec.
"""
def __init__(self, message, path=None, element=None):
self.message = message
self.path = path
self.element = element
self.message = message
self.path = path


class ReferenceResolutionError(SpecError):
"""
Expand Down
14 changes: 9 additions & 5 deletions openapi3/general.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
from .object_base import ObjectBase


class ExternalDocumentation(ObjectBase):
"""
An `External Documentation Object`_ references external resources for extended
documentation.
.. _External Documentation Object: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#externalDocumentationObject
"""
__slos__ = ['description','url']
__slos__ = ['description', 'url']
required_fields = 'url'

def _parse_data(self):
self.description = self._get('description', str)
self.url = self._get('url', str)
self.url = self._get('url', str)


class Reference(ObjectBase):
"""
A `Reference Object`_ designates a reference to another node in the specification.
.. _Reference Object: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#referenceObject
"""
__slots__ = ['ref'] # can't start a variable name with a $
# can't start a variable name with a $
__slots__ = ['ref']
required_fields = ['$ref']

def _parse_data(self):
Expand All @@ -32,6 +35,7 @@ def can_parse(cls, dct):
Override ObjectBase.can_parse because we had to remove the $ from $ref
in __slots__ (since that's not a valid python variable name)
"""
cleaned_keys = [k for k in dct.keys() if not k.startswith('x-')] # TODO - can a reference object
# have spec extensions?
# TODO - can a reference object have spec extensions?
cleaned_keys = [k for k in dct.keys() if not k.startswith('x-')]

return len(cleaned_keys) == 1 and '$ref' in dct
28 changes: 16 additions & 12 deletions openapi3/info.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,57 @@
from .object_base import ObjectBase


class Info(ObjectBase):
"""
An OpenAPI Info object, as defined in `the spec`_.
.. _the spec: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#infoObject
"""
__slots__ = ['title','description','termsOfService','contact','license','version']
required_fields = ['title','version']
__slots__ = ['title', 'description', 'termsOfService', 'contact',
'license', 'version']
required_fields = ['title', 'version']

def _parse_data(self):
"""
Implementation of :any:`ObjectBase._parse_data`
"""
self.title = self._get('title', str)
self.description = self._get('description', str)
self.contact = self._get('contact', 'Contact')
self.description = self._get('description', str)
self.license = self._get('license', 'License')
self.termsOfService = self._get('termsOfService', str)
self.contact = self._get('contact', 'Contact')
self.license = self._get('license', 'License')
self.version = self._get('version', str)
self.title = self._get('title', str)
self.version = self._get('version', str)


class Contact(ObjectBase):
"""
Contact object belonging to an Info object, as described `here`_
.. _here: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#contactObject
"""
__slots__ = ['name','url','email']
__slots__ = ['name', 'url', 'email']

def _parse_data(self):
"""
Implementation of :any:`ObjectBase._parse_data`
"""
self.name = self._get('name', str)
self.url = self._get('url', str)
self.email = self._get('email', str)
self.name = self._get('name', str)
self.url = self._get('url', str)


class License(ObjectBase):
"""
License object belonging to an Info object, as described `here`_
.. _here: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#license-object
"""
__slots__ = ['name','url']
__slots__ = ['name', 'url']
required_fields = ['name']

def _parse_data(self):
"""
Implementation of :any:`ObjectBase._parse_data`
"""
self.name = self._get('name', str)
self.url = self._get('url', str)
self.url = self._get('url', str)
57 changes: 33 additions & 24 deletions openapi3/object_base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from .errors import SpecError, ReferenceResolutionError


class ObjectBase(object):
"""
The base class for all schema objects. Includes helpers for common schema-
related functions.
"""
__slots__ = ['path','raw_element','_accessed_members','strict','extensions',
'_root', '_original_ref']
__slots__ = ['path', 'raw_element', '_accessed_members', 'strict', '_root',
'extensions', '_original_ref']
required_fields = []

def __init__(self, path, raw_element, root):
Expand Down Expand Up @@ -34,9 +35,11 @@ def __init__(self, path, raw_element, root):
self._root = root

self._accessed_members = []
self.strict = False # TODO - add a strict mode that errors if all members were not accessed
self.extensions = {}

# TODO - add strict mode that errors if all members were not accessed
self.strict = False

# parse our own element
try:
self._required_fields(*type(self).required_fields)
Expand All @@ -47,7 +50,8 @@ def __init__(self, path, raw_element, root):
else:
raise

self._parse_spec_extensions() # TODO - this may not be appropriate in all cases
# TODO - this may not be appropriate in all cases
self._parse_spec_extensions()

# TODO - assert that all keys of raw_element were accessed

Expand All @@ -56,7 +60,7 @@ def __repr__(self):
Returns a string representation of the parsed object
"""
# TODO - why?
return str(self.__dict__()) # pylint: disable=not-callable
return str(self.__dict__()) # pylint: disable=not-callable

def __dict__(self):
"""
Expand Down Expand Up @@ -125,12 +129,13 @@ def _get(self, field, object_types, is_list=False, is_map=False):
"""
self._accessed_members.append(field)

ret = self.raw_element.get(field, None)
ret = self.raw_element.get(field, None)

try:
if ret is not None:
if not isinstance(object_types, list):
object_types = [object_types] # maybe don't accept not-lists
# maybe don't accept not-lists
object_types = [object_types]

if is_list:
if not isinstance(ret, list):
Expand All @@ -147,22 +152,23 @@ def _get(self, field, object_types, is_list=False, is_map=False):
type(ret)),
path=self.path,
element=self)
ret = Map(self.path+[field], ret, object_types, self._root)
ret = Map(self.path + [field], ret, object_types, self._root)
else:
accepts_string = str in object_types
found_type = False

for t in object_types:
if t == str:
continue # try to parse everything else first
# try to parse everything else first
continue

if isinstance(t, str):
# we were given the name of a subclass of ObjectBase,
# attempt to parse ret as that type
python_type = ObjectBase.get_object_type(t)

if python_type.can_parse(ret):
ret = python_type(self.path+[field], ret, self._root)
ret = python_type(self.path + [field], ret, self._root)
found_type = True
break
elif isinstance(ret, t):
Expand Down Expand Up @@ -254,10 +260,10 @@ def get_object_type(cls, typename):
setattr(cls, '_subclass_map', {t.__name__: t for t in cls.__subclasses__()})

# TODO - why?
if typename not in cls._subclass_map: # pylint: disable=no-member
if typename not in cls._subclass_map: # pylint: disable=no-member
raise ValueError('ObjectBase has no subclass {}'.format(typename))

return cls._subclass_map[typename] # pylint: disable=no-member
return cls._subclass_map[typename] # pylint: disable=no-member

def get_path(self):
"""
Expand Down Expand Up @@ -305,7 +311,7 @@ def parse_list(self, raw_list, object_types, field=None):

for cur_type in python_types:
if issubclass(cur_type, ObjectBase) and cur_type.can_parse(cur):
result.append(cur_type(real_path+[str(i)], cur, self._root))
result.append(cur_type(real_path + [str(i)], cur, self._root))
found_type = True
continue
elif isinstance(cur, cur_type):
Expand All @@ -326,7 +332,8 @@ def _resolve_references(self):
Resolves all reference objects below this object and notes their original
value was a reference.
"""
reference_type = ObjectBase.get_object_type('Reference') # don't circular import
# don't circular import
reference_type = ObjectBase.get_object_type('Reference')

for slot in self.__slots__:
if slot.startswith('_'):
Expand All @@ -353,11 +360,12 @@ def _resolve_references(self):
e.element = self
raise

resolved_value._original_ref = value # TODO - this will break if
# multiple things reference
# the same node.
# FIXME - will break if multiple things reference the same
# node
resolved_value._original_ref = value

setattr(self, slot, resolved_value) # resolved
# resolved
setattr(self, slot, resolved_value)
elif issubclass(type(value), ObjectBase) or isinstance(value, Map):
# otherwise, continue resolving down the tree
value._resolve_references()
Expand Down Expand Up @@ -392,7 +400,7 @@ class Map(dict):
The Map object wraps a python dict and parses its values into the chosen
type or types.
"""
__slots__ = ['dct','path','raw_element','_root']
__slots__ = ['dct', 'path', 'raw_element', '_root']

def __init__(self, path, raw_element, object_types, root):
"""
Expand Down Expand Up @@ -426,7 +434,7 @@ def __init__(self, path, raw_element, object_types, root):

for t in python_types:
if issubclass(t, ObjectBase) and t.can_parse(v):
dct[k] = t(path+[k], v, self._root)
dct[k] = t(path + [k], v, self._root)
found_type = True
elif isinstance(v, t):
dct[k] = v
Expand Down Expand Up @@ -469,10 +477,11 @@ def _resolve_references(self):
e.element = self
raise

resolved_value._original_ref = value # TODO - this will break if
# multiple things reference
# the same node.
# FIXME - will break if multiple things reference the same
# node
resolved_value._original_ref = value

self[key] = resolved_value # resolved
# resolved
self[key] = resolved_value
else:
value._resolve_references()
Loading

0 comments on commit 1ca675f

Please sign in to comment.