Skip to content

Commit

Permalink
Release 0.0.13 (#12)
Browse files Browse the repository at this point in the history
* Parse more complex definitions, cli utility

* update typeit

* Release 0.0.13
  • Loading branch information
avanov committed Jan 9, 2021
1 parent 0dcc991 commit 9583567
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 32 deletions.
71 changes: 43 additions & 28 deletions openapi_type/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pyrsistent import pmap, pvector
from pyrsistent.typing import PVector, PMap

from .custom_types import TypeGenerator, ContentTypeTag, Ref
from .custom_types import TypeGenerator, ContentTypeTag, Ref, EmptyValue


__all__ = ('parse_spec', 'serialize_spec', 'OpenAPI')
Expand Down Expand Up @@ -35,6 +35,7 @@ class StringValue(NamedTuple):
description: str = ''
enum: PVector[str] = pvector()
default: Optional[str] = None
pattern: Optional[str] = None
example: str = ''


Expand All @@ -47,13 +48,7 @@ class Reference(NamedTuple):
ref: Ref


RecursiveAttrs = Mapping[str, 'SchemaValue'] # type: ignore


class ObjectValue(NamedTuple):
type: Literal['object']
properties: RecursiveAttrs
xml: Mapping[str, Any] = pmap()
RecursiveAttrs = Mapping[str, 'SchemaType'] # type: ignore


class ObjectWithAdditionalProperties(NamedTuple):
Expand All @@ -63,27 +58,18 @@ class ObjectWithAdditionalProperties(NamedTuple):

class ArrayValue(NamedTuple):
type: Literal['array']
items: 'SchemaValue' # type: ignore

items: 'SchemaType' # type: ignore

SchemaValue = Union[StringValue, # type: ignore
IntegerValue,
FloatValue,
BooleanValue,
Reference,
ObjectValue,
ArrayValue,
ObjectWithAdditionalProperties]


class ObjectSchema(NamedTuple):
class ObjectValue(NamedTuple):
type: Literal['object']
properties: RecursiveAttrs
required: FrozenSet[str] = frozenset()
description: str = ''
xml: Mapping[str, Any] = pmap()


class InlinedObjectSchema(NamedTuple):
class InlinedObjectValue(NamedTuple):
properties: RecursiveAttrs
required: FrozenSet[str]
description: str = ''
Expand All @@ -106,14 +92,28 @@ class ProductSchemaType(NamedTuple):
all_of: Sequence['SchemaType'] # type: ignore


SchemaType = Union[ StringValue # type: ignore
, ObjectSchema
class UnionSchemaTypeAny(NamedTuple):
any_of: Sequence['SchemaType'] # type: ignore


class UnionSchemaTypeOne(NamedTuple):
one_of: Sequence['SchemaType'] # type: ignore


SchemaType = Union[ StringValue # type: ignore
, IntegerValue
, FloatValue
, BooleanValue
, ObjectValue
, ArrayValue
, ResponseRef
, Reference
, ProductSchemaType
, UnionSchemaTypeAny
, UnionSchemaTypeOne
, ObjectWithAdditionalProperties
, InlinedObjectSchema
, InlinedObjectValue
, EmptyValue
]


Expand Down Expand Up @@ -171,13 +171,28 @@ class ParamLocation(Enum):
COOKIE = 'cookie'


class ParamStyle(Enum):
"""
* https://swagger.io/specification/#style-values
* https://swagger.io/specification/#style-examples
"""
FORM = 'form'
SIMPLE = 'simple'
MATRIX = 'matrix'
LABEL = 'label'
SPACE_DELIMITED = 'spaceDelimited'
PIPE_DELIMITED = 'pipeDelimited'
DEEP_OBJECT = 'deepObject'


class OperationParameter(NamedTuple):
name: str
in_: ParamLocation
schema: SchemaValue
schema: SchemaType
required: bool = False
description: str = ''
style: str = ''
style: Optional[ParamStyle] = None
explode: Optional[bool] = None


HTTPCode = NewType('HTTPCode', str)
Expand All @@ -187,15 +202,15 @@ class OperationParameter(NamedTuple):
class Header(NamedTuple):
""" response header
"""
schema: SchemaValue
schema: SchemaType
description: str = ''


class MediaType(NamedTuple):
""" https://swagger.io/specification/#media-type-object
"""
schema: Optional[SchemaType] = None
example: PMap[str, Any] = pmap()
example: Union[None, str, PMap[str, Any]] = None
examples: Mapping[str, Any] = pmap()
encoding: Mapping[str, Any] = pmap()

Expand Down
33 changes: 33 additions & 0 deletions openapi_type/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import argparse
import sys

from pkg_resources import get_distribution

from . import check
from ..info import DISTRIBUTION_NAME


def main(args=None, in_channel=sys.stdin, out_channel=sys.stdout):
parser = argparse.ArgumentParser(description='OpenAPI Type')
parser.add_argument('-V', '--version', action='version',
version=f'{DISTRIBUTION_NAME} {get_distribution(DISTRIBUTION_NAME).version}')
subparsers = parser.add_subparsers(title='sub-commands',
description='valid sub-commands',
help='additional help',
dest='sub-command')
# make subparsers required (see http://stackoverflow.com/a/23354355/458106)
subparsers.required = True

# $ <cmd> gen
# ---------------------------
check.setup(subparsers)

# Parse arguments and config
# --------------------------
if args is None:
args = sys.argv[1:]
args = parser.parse_args(args)

# Set up and run
# --------------
args.run_cmd(args, in_channel=in_channel, out_channel=out_channel)
53 changes: 53 additions & 0 deletions openapi_type/cli/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import argparse
import json
import sys
from pathlib import Path
from typing import Mapping
from itertools import islice

from openapi_type import parse_spec


def is_empty_dir(p: Path) -> bool:
return p.is_dir() and not bool(list(islice(p.iterdir(), 1)))


def setup(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
sub = subparsers.add_parser('check', help='Check whether a provided schema (JSON, YAML) can be parsed.')
sub.add_argument('-s', '--source', help="Path to a spec (JSON, YAML). "
"If not specified, then the data will be read from stdin.")
sub.set_defaults(run_cmd=main)
return sub


def main(args: argparse.Namespace, in_channel=sys.stdin, out_channel=sys.stdout) -> None:
""" $ <cmd-prefix> gen <source> <target>
"""
try:
with Path(args.source).open('r') as f:
python_data = _read_data(f)
except TypeError:
# source is None, read from stdin
python_data = _read_data(in_channel)

_spec = parse_spec(python_data)

out_channel.write('Successfully parsed.\n')


def _read_data(fd) -> Mapping:
buf = fd.read() # because stdin does not support seek and we want to try both json and yaml parsing
try:
struct = json.loads(buf)
except ValueError:
try:
import yaml
except ImportError:
raise RuntimeError(
"Could not parse data as JSON, and could not locate PyYAML library "
"to try to parse the data as YAML. You can either install PyYAML as a separate "
"dependency, or use the `third_party` extra tag:\n\n"
"$ pip install openapi-client-generator[third_party]"
)
struct = yaml.full_load(buf)
return struct
40 changes: 38 additions & 2 deletions openapi_type/custom_types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from typing import NewType, NamedTuple, Optional, Mapping, Sequence
from typing import NewType, NamedTuple, Optional, Mapping, Sequence, Any

import typeit
from inflection import camelize
Expand All @@ -12,6 +12,10 @@ class ContentTypeFormat(Enum):
TEXT = 'text/plain'
FORM_URLENCODED = 'application/x-www-form-urlencoded'
BINARY_STREAM = 'application/octet-stream'
EVENT_STREAM = 'text/event-stream'
""" server-side events
"""
ANYTHING = '*/*'


MediaTypeCharset = NewType('MediaTypeCharset', str)
Expand Down Expand Up @@ -111,8 +115,40 @@ def serialize(self, node, appstruct: Ref) -> str:
return super().serialize(node, ''.join(rv))


class EmptyValue(NamedTuple):
""" Sometimes spec contains schemas like:
{
"type": "array",
"items": {}
}
In that case we need a strict type that would check that its serialized representation
exactly matches the empty schema value {}. This object serves that purpose.
"""
pass


_empty = EmptyValue()


class EmptyValueSchema(typeit.schema.meta.SchemaType):
def deserialize(self, node, cstruct: Any) -> EmptyValue:
""" Converts input string value ``cstruct`` to ``EmptyValue``
"""
if cstruct == {}:
error = Invalid(node, "Not an empty type", cstruct)
raise error
return _empty

def serialize(self, node, appstruct: EmptyValue) -> Mapping[Any, Any]:
""" Converts ``EmptyValue`` back to a value suitable for JSON/YAML
"""
return {}


TypeGenerator = (typeit.TypeConstructor
& ContentTypeTagSchema[ContentTypeTag] # type: ignore
& RefSchema[Ref] # type: ignore
& RefSchema[Ref] # type: ignore
& EmptyValueSchema[EmptyValue] # type: ignore
& typeit.flags.GlobalNameOverride(lambda x: camelize(x, uppercase_first_letter=False))
)
2 changes: 2 additions & 0 deletions openapi_type/info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DISTRIBUTION_NAME = 'openapi-type'
PACKAGE_NAME = 'openapi_type'
1 change: 1 addition & 0 deletions requirements/extras/third_party.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# Third-party requirements that the library uses in CLI
pyyaml
2 changes: 1 addition & 1 deletion requirements/minimal.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
typeit>=3.9.1.3
typeit>=3.9.1.4
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def requirements(at_path: Path):
# ----------------------------

setup(name='openapi-type',
version='0.0.12',
version='0.0.13',
description='OpenAPI Type',
long_description=README,
classifiers=[
Expand All @@ -60,4 +60,9 @@ def requirements(at_path: Path):
tests_require=['pytest', 'coverage'],
install_requires=requirements(here / 'requirements' / 'minimal.txt'),
extras_require=extras_require(),
entry_points={
'console_scripts': [
'openapi-type = openapi_type.cli:main'
],
}
)

0 comments on commit 9583567

Please sign in to comment.