From aaf3fd13145e312244b5d639d048ea6585beaa86 Mon Sep 17 00:00:00 2001 From: Theron Luhn Date: Mon, 17 Dec 2018 14:04:28 -0800 Subject: [PATCH] Update OpenAPI tool and document it. --- README.md | 105 +++++++++++++++++- poetry.lock | 4 +- pyproject.toml | 4 +- .../{scripts.py => generatespec.py} | 24 ++-- pyramid_marshmallow/spec.py | 2 +- 5 files changed, 120 insertions(+), 19 deletions(-) rename pyramid_marshmallow/{scripts.py => generatespec.py} (84%) diff --git a/README.md b/README.md index 6c86859..888caf2 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ You can also get a schema made from a dictionary by using the fields. -## Error handling +### Error handling If the validation fails, a `pyramid_marshmallow.ValidationError` is raised. The `errors` property of the exception contains a dictionary of error messages, @@ -106,3 +106,106 @@ A failure during marshalling will result in a `pyramid_marshmallow.MarshalError` which behaves in the same manner. It's usually less useful to attach a view to that exception, since marshalling errors are usually not encountered during standard operation. + +## OpenAPI + +By adding validation and marshalling to your views, we have the opportunity to +utilize that data to generate documentation. pyramid-marshmallow includes an +utility that uses [apispec](https://apispec.readthedocs.io/en/stable/) to +generate an [OpenAPI](https://swagger.io/resources/open-api/) specification for +your application. + +First, you'll need to install some extra dependencies. + +```bash +pip install pyramid-marshmallow[openapi] +``` + +Now you can generate your spec by simply passing in an ini file. +pyramid-marshmallow needs to run your application in order to inspect it, so +the ini file should contain all the necessary configuration to do so. + +```bash +generate-spec development.ini +``` + +This will output the spec to stdout as JSON. You can set the `--output` flag +to output the results to a file. + +You can set `--format yaml` to output the spec as YAML instead or +`--format zip` to output a zip file containing the spec and +[Swagger UI](https://swagger.io/tools/swagger-ui/), a web interface for viewing +the spec. + +By default, your spec will be titled "Untitled" and versioned "0.1.0". You can +change this by setting `openapi.title` and `openapi.version` in your ini file. + +### Additional Documentation + +To add additional documentation to schema fields, you can set the `description` +property. + +```python +class Hello(Schema): + name = String(required=True, description='Your first and last name.') +``` + +Documentation for the endpoint will be pulled from the view callable's +docstring. + +You can also augment the spec by adding a line of three hyphens followed by +YAML. The YAML will be parsed and merged into to the endpoint's spec. This +can be useful for documenting endpoints that cannot be validated or marshalled, +such as endpoints that return an empty body. + +```python +@view_config( + context=WidgetResource, + method='post', + validate=WidgetSchema(), +) +def create_widget(context, request): + """ + Create a new widget. + --- + responses: + 201: + description: Indicates the widget was successfully created. + """ + create_widget() + return HTTPCreated() +``` + +## URL Traversal + +If you're using Pyramid's URL traversal, the generated spec may be mostly +empty. This is because pyramid-marshmallow has no way of knowing where in the +resource tree a resource is. You can denote this by setting the `__path__` +property on each resource. + +```python +class Widget(Resource): + __path__ = '/widget' +``` + +Views attached to this resource will then be added to the spec. + +You can add parameters to your path via the `__params__` property. You can +also tag all attached views via `__tag__`. Once you define a tag in one +resource, you can use it elsewhere by setting `__tag__` to the tag name. + +```python +class Widget(Resource): + __path__ = '/widget/{widgetId}' + __params__ = [{ + 'name': 'widgetId', + 'schema': { + 'type': 'integer', + }, + }] + __tag__ = { + 'name': 'widgets', + 'description': 'Endpoints for managing a widget.', + } +``` + diff --git a/poetry.lock b/poetry.lock index ae31b37..a9694aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -305,10 +305,10 @@ version = "4.6.0" setuptools = "*" [extras] -apispec = ["apispec"] +openapi = ["apispec", "PyYAML"] [metadata] -content-hash = "5ad667478a8eb75f6ad1c0fb81017dca7274d232b9584a02727535e5f1802839" +content-hash = "cdf25c5278e0ade4b518731721357860b4e81c88e95276e15ecdd6c9d981da93" python-versions = "^3.4" [metadata.hashes] diff --git a/pyproject.toml b/pyproject.toml index aa171ca..13aa527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,10 +27,10 @@ pytest = "^3.6" webtest = "^2.0" [tool.poetry.extras] -apispec = ["apispec", "PyYAML"] +openapi = ["apispec", "PyYAML"] [tool.poetry.scripts] -generate-apispec = "pyramid_marshmallow.scripts:generate" +generate-spec = "pyramid_marshmallow.generatespec:generate" [build-system] requires = ["poetry>=0.12"] diff --git a/pyramid_marshmallow/scripts.py b/pyramid_marshmallow/generatespec.py similarity index 84% rename from pyramid_marshmallow/scripts.py rename to pyramid_marshmallow/generatespec.py index e5eed85..7b183c9 100644 --- a/pyramid_marshmallow/scripts.py +++ b/pyramid_marshmallow/generatespec.py @@ -8,23 +8,14 @@ parser = argparse.ArgumentParser() -parser.add_argument( - 'format', - help='The output, one of "json", "yaml", or "zip".', -) parser.add_argument( 'ini', help='The .ini config file for the Pyramid project.', ) parser.add_argument( - '--title', - default='Untitled', - help='The title for the spec.', -) -parser.add_argument( - '--version', - default='1.0.0', - help='The version for the spec.', + '--format', + help='The output, one of "json", "yaml", or "zip".', + default='json', ) parser.add_argument( '--output', @@ -37,8 +28,11 @@ def generate(): from .spec import create_spec args = parser.parse_args() app = get_app(args.ini) + settings = app.registry.settings + title = settings.get('openapi.title', 'Untitled') + version = settings.get('openapi.version', '0.0.0') introspector = app.registry.introspector - spec = create_spec(args.title, args.version, introspector) + spec = create_spec(title, version, introspector) if args.format == 'json': output = json.dumps(spec.to_dict()) elif args.format == 'yaml': @@ -67,3 +61,7 @@ def generate_zip(spec): with zipfile.ZipFile(fh, 'a') as zip: zip.writestr('swagger.json', swaggerjson) return fh.getvalue() + + +if __name__ == '__main__': + generate() diff --git a/pyramid_marshmallow/spec.py b/pyramid_marshmallow/spec.py index 6780984..6d34146 100644 --- a/pyramid_marshmallow/spec.py +++ b/pyramid_marshmallow/spec.py @@ -7,7 +7,7 @@ except ImportError: raise ImportError( 'You must have the `apispec` package installed to use this feature. ' - 'You can install it with `pip install pyramid_marshmallow[apispec].' + 'You can install it with `pip install pyramid_marshmallow[openapi].' )