Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add OpenAPI Specification 3.1.0 support #230

Merged
merged 7 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 42 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
## Validate [Pyramid](https://trypyramid.com) views against an [OpenAPI 3.0](https://swagger.io/specification/) document
# pyramid_openapi3

## Validate [Pyramid](https://trypyramid.com) views against [OpenAPI 3.0/3.1](https://swagger.io/specification/) documents

<p align="center">
<img height="200" src="https://github.com/Pylons/pyramid_openapi3/blob/main/header.jpg?raw=true" />
<img alt="Pyramid and OpenAPI logos"
height="200"
src="https://github.com/Pylons/pyramid_openapi3/blob/main/header.jpg?raw=true">
</p>

<p align="center">
<a href="https://circleci.com/gh/Pylons/pyramid_openapi3">
<img alt="CircleCI for pyramid_openapi3 (main branch)"
src="https://circleci.com/gh/Pylons/pyramid_openapi3.svg?style=shield">
<a href="https://github.com/Pylons/pyramid_openapi3/actions/workflows/ci.yml">
<img alt="CI for pyramid_openapi3 (main branch)"
src="https://github.com/Pylons/pyramid_openapi3/actions/workflows/ci.yml/badge.svg">
</a>
<img alt="Test coverage (main branch)"
src="https://img.shields.io/badge/tests_coverage-100%25-brightgreen.svg">
Expand Down Expand Up @@ -51,27 +55,27 @@ The reason this package exists is to give you peace of mind when providing a RES

- Validates your API document (for example, `openapi.yaml` or `openapi.json`) against the OpenAPI 3.0 specification using the [openapi-spec-validator](https://github.com/p1c2u/openapi-spec-validator).
- Generates and serves the [Swagger try-it-out documentation](https://swagger.io/tools/swagger-ui/) for your API.
- Validates incoming requests *and* outgoing responses against your API document using [openapi-core](https://github.com/p1c2u/openapi-core).
- Validates incoming requests _and_ outgoing responses against your API document using [openapi-core](https://github.com/p1c2u/openapi-core).

## Getting started

1. Declare `pyramid_openapi3` as a dependency in your Pyramid project.

2. Include the following lines:

```python
config.include("pyramid_openapi3")
config.pyramid_openapi3_spec('openapi.yaml', route='/api/v1/openapi.yaml')
config.pyramid_openapi3_add_explorer(route='/api/v1/')
```
```python
config.include("pyramid_openapi3")
config.pyramid_openapi3_spec('openapi.yaml', route='/api/v1/openapi.yaml')
config.pyramid_openapi3_add_explorer(route='/api/v1/')
```

3. Use the `openapi` [view predicate](https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/viewconfig.html#view-configuration-parameters) to enable request/response validation:

```python
@view_config(route_name="foobar", openapi=True, renderer='json')
def myview(request):
return request.openapi_validated.parameters
```
```python
@view_config(route_name="foobar", openapi=True, renderer='json')
def myview(request):
return request.openapi_validated.parameters
```

For requests, `request.openapi_validated` is available with two fields: `parameters` and `body`.
For responses, if the payload does not match the API document, an exception is raised.
Expand All @@ -80,7 +84,7 @@ For responses, if the payload does not match the API document, an exception is r

### Relative File References in Spec

A feature introduced in OpenAPI3 is the ability to use `$ref` links to external files (https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#referenceObject).
A feature introduced in OpenAPI3 is the ability to use `$ref` links to external files (<https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#reference-object>).

To use this, you must ensure that you have all of your spec files in a given directory (ensure that you do not have any code in this directory as all the files in it are exposed as static files), then **replace** the `pyramid_openapi3_spec` call that you did in [Getting Started](#getting-started) with the following:

Expand All @@ -97,9 +101,10 @@ Some notes:
### Endpoints / Request / Response Validation

Provided with `pyramid_openapi3` are a few validation features:
* incoming request validation (i.e., what a client sends to your app)
* outgoing response validation (i.e., what your app sends to a client)
* endpoint validation (i.e., your app registers routes for all defined API endpoints)

- incoming request validation (i.e., what a client sends to your app)
- outgoing response validation (i.e., what your app sends to a client)
- endpoint validation (i.e., your app registers routes for all defined API endpoints)

These features are enabled as a default, but you can disable them if you need to:

Expand All @@ -109,8 +114,8 @@ config.registry.settings["pyramid_openapi3.enable_request_validation"] = False
config.registry.settings["pyramid_openapi3.enable_response_validation"] = False
```

> **Warning:**
Disabling request validation will result in `request.openapi_validated` no longer being available to use.
> [!WARNING]
> Disabling request validation will result in `request.openapi_validated` no longer being available to use.

### Register Pyramid's Routes

Expand Down Expand Up @@ -145,37 +150,37 @@ The `pyramid_openapi3_register_routes()` method supports setting a factory and r

Sometimes, it is necessary to specify the protocol and port to access the openapi3 spec file. This can be configured using the `proto_port` optional parameter to the the `pyramid_openapi3_add_explorer` function:

config.pyramid_openapi3_add_explorer(proto_port=('https', 443))

```python
config.pyramid_openapi3_add_explorer(proto_port=('https', 443))
```

## Demo / Examples

There are three examples provided with this package:
* A fairly simple [single-file app providing a Hello World API](https://github.com/Pylons/pyramid_openapi3/tree/main/examples/singlefile).
* A slightly more [built-out app providing a TODO app API](https://github.com/Pylons/pyramid_openapi3/tree/main/examples/todoapp).
* Another TODO app API, defined using a [YAML spec split into multiple files](https://github.com/Pylons/pyramid_openapi3/tree/main/examples/splitfile).

- A fairly simple [single-file app providing a Hello World API](https://github.com/Pylons/pyramid_openapi3/tree/main/examples/singlefile).
- A slightly more [built-out app providing a TODO app API](https://github.com/Pylons/pyramid_openapi3/tree/main/examples/todoapp).
- Another TODO app API, defined using a [YAML spec split into multiple files](https://github.com/Pylons/pyramid_openapi3/tree/main/examples/splitfile).

All examples come with tests that exhibit pyramid_openapi's error handling and validation capabilities.

A **fully built-out app**, with 100% test coverage, providing a [RealWorld.io](https://realworld.io) API is available at [niteoweb/pyramid-realworld-example-app](https://github.com/niteoweb/pyramid-realworld-example-app). It is a Heroku-deployable Pyramid app that provides an API for a Medium.com-like social app. You are encouraged to use it as a scaffold for your next project.


## Design defense

The authors of pyramid_openapi3 believe that the approach of validating a manually-written API document is superior to the approach of generating the API document from Python code. Here are the reasons:

1. Both generation and validation against a document are lossy processes. The underlying libraries running the generation/validation will always have something missing. Either a feature from the latest OpenAPI specification, or an implementation bug. Having to fork the underlying library in order to generate the part of your API document that might only be needed for the frontend is unfortunate.
1. Both generation and validation against a document are lossy processes. The underlying libraries running the generation/validation will always have something missing. Either a feature from the latest OpenAPI specification, or an implementation bug. Having to fork the underlying library in order to generate the part of your API document that might only be needed for the frontend is unfortunate.

Validation on the other hand allows one to skip parts of validation that are not supported yet, and not block a team from shipping the document.

2. The validation approach does sacrifice DRY-ness, and one has to write the API document and then the (view) code in Pyramid. It feels a bit redundant at first. However, this provides a clear separation between the intent and the implementation.

3. The generation approach has the drawback of having to write Python code even for parts of the API document that the Pyramid backend does not handle, as it might be handled by a different system, or be specific only to documentation or only to the client side of the API. This bloats your Pyramid codebase with code that does not belong there.
2. The validation approach does sacrifice DRY-ness, and one has to write the API document and then the (view) code in Pyramid. It feels a bit redundant at first. However, this provides a clear separation between the intent and the implementation.

3. The generation approach has the drawback of having to write Python code even for parts of the API document that the Pyramid backend does not handle, as it might be handled by a different system, or be specific only to documentation or only to the client side of the API. This bloats your Pyramid codebase with code that does not belong there.

## Running tests

You need to have [poetry](https://python-poetry.org/) and Python 3.9 through 3.12 installed on your machine. All `Makefile` commands assume you have the Poetry environment activated, i.e. `poetry shell`.
You need to have [poetry](https://python-poetry.org/) and Python 3.9 & 3.12 installed on your machine. All `Makefile` commands assume you have the Poetry environment activated, i.e. `poetry shell`.

Alternatively, if you use [nix](https://nix.dev/tutorials/declarative-and-reproducible-developer-environments), run `nix-shell` to drop into a shell that has everything prepared for development.

Expand All @@ -201,10 +206,10 @@ These packages tackle the same problem-space:

We do our best to follow the rules below.

* Support the latest few releases of Python, currently Python 3.9 through 3.12.
* Support the latest few releases of Pyramid, currently 1.10.7 through 2.0.2.
* Support the latest few releases of `openapi-core`, currently just 0.19.0.
* See `poetry.lock` for a frozen-in-time known-good-set of all dependencies.
- Support the latest few releases of Python, currently Python 3.9 through 3.12.
- Support the latest few releases of Pyramid, currently 1.10.7 through 2.0.2.
- Support the latest few releases of `openapi-core`, currently just 0.19.0.
- See `poetry.lock` for a frozen-in-time known-good-set of all dependencies.

## Use in the wild

Expand Down
2 changes: 1 addition & 1 deletion examples/singlefile/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# example we want everything in a single file. Other examples have it nicely
# separated.
OPENAPI_DOCUMENT = b"""
openapi: "3.0.0"
openapi: "3.1.0"
info:
version: "1.0.0"
title: Hello API
Expand Down
2 changes: 1 addition & 1 deletion examples/splitfile/spec/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: "3.0.0"
openapi: "3.1.0"

info:
version: "1.0.0"
Expand Down
2 changes: 1 addition & 1 deletion examples/todoapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ All of these examples are covered with tests that you can run with `$ python -m

* More information about the library providing the integration between OpenAPI specs and Pyramid, more advanced features and design defence, is available in the main [README](https://github.com/Pylons/pyramid_openapi3) file.

* More validators for fields are listed in the [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#properties) document. You can use Regex as well.
* More validators for fields are listed in the [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#properties) document. You can use Regex as well.

* For an idea of a fully-fledged production OpenApi specification, check out [WooCart's OpenAPI docs](https://app.woocart.com/api/v1/).
2 changes: 1 addition & 1 deletion examples/todoapp/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
openapi: "3.0.0"
openapi: "3.1.0"

info:
version: "1.0.0"
Expand Down
22 changes: 19 additions & 3 deletions pyramid_openapi3/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
from .wrappers import PyramidOpenAPIRequest
from jsonschema_path import SchemaPath
from openapi_core.unmarshalling.request import V30RequestUnmarshaller
from openapi_core.unmarshalling.request import V31RequestUnmarshaller
from openapi_core.unmarshalling.response import V30ResponseUnmarshaller
from openapi_core.unmarshalling.response import V31ResponseUnmarshaller
from openapi_core.validation.request.exceptions import SecurityValidationError
from openapi_spec_validator import validate
from openapi_spec_validator.readers import read_from_filename
from openapi_spec_validator.versions.shortcuts import get_spec_version
from pathlib import Path
from pyramid.config import Configurator
from pyramid.config import PHASE0_CONFIG
Expand Down Expand Up @@ -142,7 +145,7 @@ def add_explorer_view(
route: str = "/docs/",
route_name: str = "pyramid_openapi3.explorer",
template: str = "static/index.html",
ui_version: str = "4.18.3",
ui_version: str = "5.12.0",
permission: str = NO_PERMISSION_REQUIRED,
apiname: str = "pyramid_openapi3",
proto_port: t.Optional[t.Tuple[str, int]] = None,
Expand Down Expand Up @@ -310,16 +313,29 @@ def _create_api_settings(
"pyramid_openapi3_deserializers"
)

# switch unmarshaller based on spec version
spec_version = get_spec_version(spec.contents())
request_unmarshallers = {
"OpenAPIV3.0": V30RequestUnmarshaller,
"OpenAPIV3.1": V31RequestUnmarshaller,
}
response_unmarshallers = {
"OpenAPIV3.0": V30ResponseUnmarshaller,
"OpenAPIV3.1": V31ResponseUnmarshaller,
}
request_unmarshaller = request_unmarshallers[str(spec_version)]
response_unmarshaller = response_unmarshallers[str(spec_version)]

return {
"filepath": filepath,
"spec_route_name": route_name,
"spec": spec,
"request_validator": V30RequestUnmarshaller(
"request_validator": request_unmarshaller(
spec,
extra_format_validators=custom_formatters,
extra_media_type_deserializers=custom_deserializers,
),
"response_validator": V30ResponseUnmarshaller(
"response_validator": response_unmarshaller(
spec,
extra_format_validators=custom_formatters,
extra_media_type_deserializers=custom_deserializers,
Expand Down
6 changes: 3 additions & 3 deletions pyramid_openapi3/tests/test_app_construction.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import typing as t

DOCUMENT = b"""
openapi: "3.0.0"
openapi: "3.1.0"
info:
version: "1.0.0"
title: Foo API
Expand All @@ -38,7 +38,7 @@
"""

SPLIT_DOCUMENT = b"""
openapi: "3.0.0"
openapi: "3.1.0"
info:
version: "1.0.0"
title: Foo API
Expand Down Expand Up @@ -70,7 +70,7 @@

# A test for when someone defines a `server.url` to just be `/`
ROOT_SERVER_DOCUMENT = b"""
openapi: "3.0.0"
openapi: "3.1.0"
info:
version: "1.0.0"
title: Foo API
Expand Down
Loading