├── .git-blame-ignore-revs ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE └── workflows │ ├── pipeline.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── LICENSE ├── NOTICE ├── README.md ├── connexion ├── __init__.py ├── __main__.py ├── apps │ ├── __init__.py │ ├── abstract.py │ ├── asynchronous.py │ └── flask.py ├── cli.py ├── context.py ├── datastructures.py ├── decorators │ ├── __init__.py │ ├── main.py │ ├── parameter.py │ └── response.py ├── exceptions.py ├── frameworks │ ├── __init__.py │ ├── abstract.py │ ├── flask.py │ └── starlette.py ├── handlers.py ├── http_facts.py ├── json_schema.py ├── jsonifier.py ├── lifecycle.py ├── middleware │ ├── __init__.py │ ├── abstract.py │ ├── context.py │ ├── exceptions.py │ ├── lifespan.py │ ├── main.py │ ├── request_validation.py │ ├── response_validation.py │ ├── routing.py │ ├── security.py │ ├── server_error.py │ └── swagger_ui.py ├── mock.py ├── operations │ ├── __init__.py │ ├── abstract.py │ ├── openapi.py │ └── swagger2.py ├── options.py ├── problem.py ├── resolver.py ├── resources │ └── schemas │ │ ├── v2.0 │ │ └── schema.json │ │ └── v3.0 │ │ └── schema.json ├── security.py ├── spec.py ├── testing.py ├── types.py ├── uri_parsing.py ├── utils.py └── validators │ ├── __init__.py │ ├── abstract.py │ ├── form_data.py │ ├── json.py │ └── parameter.py ├── docs ├── .gitignore ├── Makefile ├── _static │ ├── css │ │ └── default.css │ └── js │ │ └── guru_widget.js ├── cli.rst ├── conf.py ├── context.rst ├── cookbook.rst ├── exceptions.rst ├── images │ ├── architecture.png │ ├── logo_banner.svg │ ├── sponsors │ │ ├── Fern.png │ │ └── ML6.png │ ├── swagger_ui.png │ └── validation.excalidraw ├── index.rst ├── lifespan.rst ├── middleware.rst ├── quickstart.rst ├── request.rst ├── response.rst ├── routing.rst ├── security.rst ├── swagger_ui.rst ├── testing.rst ├── v3.rst └── validation.rst ├── examples ├── apikey │ ├── README.rst │ ├── app.py │ └── spec │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── basicauth │ ├── README.rst │ ├── app.py │ └── spec │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── enforcedefaults │ ├── README.rst │ ├── app.py │ └── spec │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── frameworks │ ├── README.rst │ ├── hello_quart.py │ ├── hello_starlette.py │ ├── requirements.txt │ └── spec │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── helloworld │ ├── README.rst │ ├── hello.py │ └── spec │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── helloworld_async │ ├── README.rst │ ├── hello.py │ └── spec │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── jwt │ ├── README.rst │ ├── app.py │ ├── requirements.txt │ └── spec │ │ └── openapi.yaml ├── methodresolver │ ├── README.rst │ ├── api │ │ ├── __init__.py │ │ └── petsview.py │ ├── app.py │ └── spec │ │ └── openapi.yaml ├── oauth2 │ ├── README.rst │ ├── app.py │ ├── mock_tokeninfo.py │ └── spec │ │ ├── mock_tokeninfo.yaml │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── oauth2_local_tokeninfo │ ├── README.rst │ ├── app.py │ └── spec │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── restyresolver │ ├── README.rst │ ├── api │ │ ├── __init__.py │ │ └── pets.py │ ├── resty.py │ └── spec │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── reverseproxy │ ├── README.rst │ ├── app.py │ └── spec │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── splitspecs │ ├── README.rst │ ├── app.py │ └── spec │ │ ├── components.yaml │ │ ├── definitions.yaml │ │ ├── openapi.yaml │ │ └── swagger.yaml └── sqlalchemy │ ├── README.rst │ ├── app.py │ ├── orm.py │ ├── requirements.txt │ └── spec │ ├── openapi.yaml │ └── swagger.yaml ├── pyproject.toml ├── tests ├── api │ ├── __init__.py │ ├── conftest.py │ ├── test_bootstrap.py │ ├── test_bootstrap_multiple_spec.py │ ├── test_cors.py │ ├── test_errors.py │ ├── test_headers.py │ ├── test_parameters.py │ ├── test_responses.py │ ├── test_schema.py │ ├── test_secure_api.py │ ├── test_swagger_ui.py │ └── test_unordered_definition.py ├── conftest.py ├── decorators │ ├── __init__.py │ ├── test_parameter.py │ ├── test_security.py │ ├── test_uri_parsing.py │ └── test_validation.py ├── fakeapi │ ├── __init__.py │ ├── auth.py │ ├── example_method_class.py │ ├── example_method_view.py │ ├── foo_bar.py │ ├── hello │ │ ├── __init__.py │ │ └── world.py │ ├── module_with_error.py │ ├── module_with_exception.py │ └── snake_case.py ├── fixtures │ ├── bad_operations │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── bad_specs │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── datetime_support │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── default_param_error │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── different_schemas │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── invalid_schema │ │ └── swagger.yaml │ ├── json_validation │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── method_view │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── missing_implementation │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── missing_op_id │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── module_does_not_exist │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── module_not_implemented │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── multiple_yaml_same_basepath │ │ ├── openapi_bye.yaml │ │ ├── openapi_greeting.yaml │ │ ├── swagger_bye.yaml │ │ └── swagger_greeting.yaml │ ├── op_error_api │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── problem │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── relative_refs │ │ ├── components.yaml │ │ ├── definitions.yaml │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── secure_api │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── secure_endpoint │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── simple │ │ ├── basepath-slash.yaml │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── snake_case │ │ ├── openapi.yaml │ │ └── swagger.yaml │ ├── unordered_definition │ │ ├── openapi.yaml │ │ └── swagger.yaml │ └── user_module_loading_error │ │ ├── openapi.yaml │ │ └── swagger.yaml ├── test_api.py ├── test_cli.py ├── test_datastructures.py ├── test_flask_encoder.py ├── test_flask_utils.py ├── test_json_validation.py ├── test_lifespan.py ├── test_middleware.py ├── test_mock.py ├── test_mock2.py ├── test_mock3.py ├── test_operation2.py ├── test_references.py ├── test_resolver.py ├── test_resolver3.py ├── test_resolver_methodview.py ├── test_utils.py └── test_validation.py └── tox.ini /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | 600ed4ed94a3c9868370aa0d6e0c40a5ebd455be 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: spec-first 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | ### Expected behaviour 6 | 7 | 8 | 9 | ### Actual behaviour 10 | 11 | 12 | 13 | ### Steps to reproduce 14 | 15 | 16 | 17 | ### Additional info: 18 | 19 | Output of the commands: 20 | 21 | - `python --version` 22 | - `pip show connexion | grep "^Version\:"` 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | Fixes # . 2 | 3 | 4 | 5 | Changes proposed in this pull request: 6 | 7 | - 8 | - 9 | - 10 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Test pipeline 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | pip install --upgrade pip 22 | pip install "poetry<2" "tox<4" "tox-gh-actions<3" "coveralls<4" 23 | - name: Test with tox 24 | run: tox 25 | - name: Coveralls 26 | run: coveralls --service github 27 | env: 28 | COVERALLS_PARALLEL: true 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | COVERALLS_FLAG_NAME: test-${{ matrix.python-version }} 31 | 32 | finish-coveralls: 33 | needs: test 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Install "coveralls<4" 37 | run: pip install coveralls 38 | - name: Coveralls Finished 39 | run: coveralls --service github --finish 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI 2 | on: 3 | push: 4 | tags: 5 | - '[0-9]+.[0-9]+.[0-9]+*' 6 | release: 7 | types: 8 | - published 9 | jobs: 10 | build-n-publish: 11 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@master 15 | 16 | - name: Set up Python 3.9 17 | uses: actions/setup-python@v1 18 | with: 19 | python-version: 3.9 20 | 21 | - name: Update version 22 | run: sed -i "s/^version = .*/version = '${{github.ref_name}}'/" pyproject.toml 23 | 24 | - name: Build a binary wheel and a source tarball 25 | run: | 26 | pip install poetry 27 | poetry build 28 | 29 | - name: Publish distribution 📦 to Test PyPI 30 | if: github.event_name == 'push' 31 | uses: pypa/gh-action-pypi-publish@v1.5.0 32 | with: 33 | user: __token__ 34 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 35 | repository_url: https://test.pypi.org/legacy/ 36 | 37 | - name: Publish distribution 📦 to PyPI if triggered by release 38 | if: github.event_name == 'release' 39 | uses: pypa/gh-action-pypi-publish@v1.5.0 40 | with: 41 | user: __token__ 42 | password: ${{ secrets.PYPI_API_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | connexion.egg-info 4 | __pycache__ 5 | .coverage 6 | *.egg 7 | htmlcov/ 8 | *.pyc 9 | .eggs 10 | .cache/ 11 | *.swp 12 | .tox/ 13 | .idea/ 14 | .vscode/ 15 | venv/ 16 | .venv/ 17 | src/ 18 | *.un~ 19 | poetry.lock 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_branch: "main" 3 | autoupdate_schedule: monthly 4 | repos: 5 | - repo: https://github.com/pycqa/flake8 6 | rev: 7.1.1 7 | hooks: 8 | - id: flake8 9 | files: "^connexion/" 10 | additional_dependencies: 11 | - flake8-rst-docstrings==0.2.3 12 | 13 | - repo: https://github.com/PyCQA/isort 14 | rev: 5.12.0 15 | hooks: 16 | - id: isort 17 | name: isort 18 | files: "^connexion/" 19 | args: ["--project", "connexion", "--check-only", "--diff"] 20 | - id: isort 21 | name: isort examples 22 | files: "^examples/" 23 | args: ["--thirdparty", "connexion", "--check-only", "--diff"] 24 | - id: isort 25 | name: isort tests 26 | files: "^tests/" 27 | args: ["--project", "conftest", "--thirdparty", "connexion", "--check-only", "--diff"] 28 | 29 | - repo: https://github.com/psf/black 30 | rev: 22.3.0 31 | hooks: 32 | - id: black 33 | name: black 34 | files: "^connexion/" 35 | args: ["connexion"] 36 | - id: black 37 | name: black examples 38 | files: "^examples/" 39 | args: ["examples"] 40 | - id: black 41 | name: black tests 42 | files: "^tests/" 43 | args: ["tests"] 44 | 45 | - repo: https://github.com/pre-commit/mirrors-mypy 46 | rev: v0.981 47 | hooks: 48 | - id: mypy 49 | files: "^connexion/" 50 | args: ["--ignore-missing-imports", "connexion"] 51 | additional_dependencies: 52 | - types-jsonschema 53 | - types-PyYAML 54 | - types-requests 55 | pass_filenames: false 56 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | build: 7 | os: "ubuntu-22.04" 8 | tools: 9 | python: "3.8" 10 | jobs: 11 | post_create_environment: 12 | # Install poetry 13 | - python -m pip install poetry 14 | # Tell poetry to not use a virtual environment 15 | - poetry config virtualenvs.create false 16 | post_install: 17 | # Install dependencies with 'docs' dependency group 18 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH python -m poetry install --with docs --all-extras 19 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Connexion 2 | Copyright 2021 Robbe Sneyders 3 | Copyright 2021 Ruwan Lambrichts 4 | Copyright 2015-2021 Zalando SE -------------------------------------------------------------------------------- /connexion/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Connexion is a framework that automagically handles HTTP requests based on OpenAPI Specification 3 | (formerly known as Swagger Spec) of your API described in YAML format. Connexion allows you to 4 | write an OpenAPI specification, then maps the endpoints to your Python functions; this makes it 5 | unique, as many tools generate the specification based on your Python code. You can describe your 6 | REST API in as much detail as you want; then Connexion guarantees that it will work as you 7 | specified. 8 | """ 9 | 10 | from .apps import AbstractApp # NOQA 11 | from .apps.asynchronous import AsyncApp 12 | from .datastructures import NoContent # NOQA 13 | from .exceptions import ProblemException # NOQA 14 | from .problem import problem # NOQA 15 | from .resolver import Resolution, Resolver, RestyResolver # NOQA 16 | from .utils import not_installed_error # NOQA 17 | 18 | try: 19 | from connexion.apps.flask import FlaskApi, FlaskApp 20 | except ImportError as e: # pragma: no cover 21 | _flask_not_installed_error = not_installed_error( 22 | e, msg="Please install connexion using the 'flask' extra" 23 | ) 24 | FlaskApi = _flask_not_installed_error # type: ignore 25 | FlaskApp = _flask_not_installed_error # type: ignore 26 | 27 | from connexion.apps.asynchronous import AsyncApi, AsyncApp 28 | from connexion.context import request 29 | from connexion.middleware import ConnexionMiddleware 30 | 31 | App = FlaskApp 32 | Api = FlaskApi 33 | -------------------------------------------------------------------------------- /connexion/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides an entrypoint for Connexion's CLI. 3 | """ 4 | 5 | from connexion.cli import main # pragma: no cover 6 | 7 | main() # pragma: no cover 8 | -------------------------------------------------------------------------------- /connexion/apps/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines Connexion applications. A Connexion App wraps a specific framework application 3 | and exposes a standardized interface for users to create and configure their Connexion application. 4 | """ 5 | 6 | from .abstract import AbstractApp # NOQA 7 | -------------------------------------------------------------------------------- /connexion/context.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | 3 | from starlette.types import Receive, Scope 4 | from werkzeug.local import LocalProxy 5 | 6 | from connexion.lifecycle import ConnexionRequest 7 | from connexion.operations import AbstractOperation 8 | 9 | UNBOUND_MESSAGE = ( 10 | "Working outside of operation context. Make sure your app is wrapped in a " 11 | "ContextMiddleware and you're processing a request while accessing the context." 12 | ) 13 | 14 | 15 | _context: ContextVar[dict] = ContextVar("CONTEXT") 16 | context = LocalProxy(_context, unbound_message=UNBOUND_MESSAGE) 17 | 18 | _operation: ContextVar[AbstractOperation] = ContextVar("OPERATION") 19 | operation = LocalProxy(_operation, unbound_message=UNBOUND_MESSAGE) 20 | 21 | _receive: ContextVar[Receive] = ContextVar("RECEIVE") 22 | receive = LocalProxy(_receive, unbound_message=UNBOUND_MESSAGE) 23 | 24 | _scope: ContextVar[Scope] = ContextVar("SCOPE") 25 | scope = LocalProxy(_scope, unbound_message=UNBOUND_MESSAGE) 26 | 27 | request = LocalProxy( 28 | lambda: ConnexionRequest(scope, receive), unbound_message=UNBOUND_MESSAGE 29 | ) 30 | -------------------------------------------------------------------------------- /connexion/datastructures.py: -------------------------------------------------------------------------------- 1 | from fnmatch import fnmatch 2 | 3 | # special marker object to return empty content for any status code 4 | # e.g. in app method do "return NoContent, 201" 5 | NoContent = object() 6 | 7 | 8 | class MediaTypeDict(dict): 9 | """ 10 | A dictionary where keys can be either media types or media type ranges. When fetching a 11 | value from the dictionary, the provided key is checked against the ranges. The most specific 12 | key is chosen as prescribed by the OpenAPI spec, with `type/*` being preferred above 13 | `*/subtype`. 14 | """ 15 | 16 | def __getitem__(self, item): 17 | # Sort keys in order of specificity 18 | for key in sorted(self, key=lambda k: ("*" not in k, k), reverse=True): 19 | if fnmatch(item, key): 20 | return super().__getitem__(key) 21 | raise super().__getitem__(item) 22 | 23 | def get(self, item, default=None): 24 | try: 25 | return self[item] 26 | except KeyError: 27 | return default 28 | 29 | def __contains__(self, item): 30 | try: 31 | self[item] 32 | except KeyError: 33 | return False 34 | else: 35 | return True 36 | -------------------------------------------------------------------------------- /connexion/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines decorators which Connexion uses to wrap user provided view functions. 3 | """ 4 | from .main import ( # noqa 5 | ASGIDecorator, 6 | FlaskDecorator, 7 | StarletteDecorator, 8 | WSGIDecorator, 9 | ) 10 | -------------------------------------------------------------------------------- /connexion/frameworks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spec-first/connexion/dd79c1146ae31be2145e224552dea95a7473e1fa/connexion/frameworks/__init__.py -------------------------------------------------------------------------------- /connexion/frameworks/abstract.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import typing as t 3 | 4 | from typing_extensions import Protocol 5 | 6 | 7 | class Framework(Protocol): 8 | @staticmethod 9 | @abc.abstractmethod 10 | def is_framework_response(response: t.Any) -> bool: 11 | """Return True if provided response is a framework type""" 12 | raise NotImplementedError 13 | 14 | @classmethod 15 | @abc.abstractmethod 16 | def connexion_to_framework_response(cls, response): 17 | """Cast ConnexionResponse to framework response class""" 18 | raise NotImplementedError 19 | 20 | @classmethod 21 | @abc.abstractmethod 22 | def build_response( 23 | cls, 24 | data: t.Any, 25 | *, 26 | content_type: t.Optional[str] = None, 27 | headers: t.Optional[dict] = None, 28 | status_code: int = None 29 | ): 30 | raise NotImplementedError 31 | 32 | @staticmethod 33 | @abc.abstractmethod 34 | def get_request(*args, **kwargs): 35 | """Return a framework request from the context.""" 36 | raise NotImplementedError 37 | 38 | @staticmethod 39 | @abc.abstractmethod 40 | def get_body(request): 41 | """Get body from a framework request""" 42 | raise NotImplementedError 43 | -------------------------------------------------------------------------------- /connexion/frameworks/starlette.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import re 3 | import typing as t 4 | 5 | import starlette.convertors 6 | from starlette.responses import JSONResponse as StarletteJSONResponse 7 | from starlette.responses import Response as StarletteResponse 8 | from starlette.types import Receive, Scope 9 | 10 | from connexion.frameworks.abstract import Framework 11 | from connexion.lifecycle import ConnexionRequest 12 | from connexion.uri_parsing import AbstractURIParser 13 | 14 | 15 | class Starlette(Framework): 16 | @staticmethod 17 | def is_framework_response(response: t.Any) -> bool: 18 | return isinstance(response, StarletteResponse) 19 | 20 | @classmethod 21 | def connexion_to_framework_response(cls, response): 22 | return cls.build_response( 23 | status_code=response.status_code, 24 | content_type=response.content_type, 25 | headers=response.headers, 26 | data=response.body, 27 | ) 28 | 29 | @classmethod 30 | def build_response( 31 | cls, 32 | data: t.Any, 33 | *, 34 | content_type: t.Optional[str] = None, 35 | headers: t.Optional[dict] = None, 36 | status_code: int = None, 37 | ): 38 | if isinstance(data, dict) or isinstance(data, list): 39 | response_cls = StarletteJSONResponse 40 | else: 41 | response_cls = StarletteResponse 42 | 43 | return response_cls( 44 | content=data, 45 | status_code=status_code, 46 | media_type=content_type, 47 | headers=headers, 48 | ) 49 | 50 | @staticmethod 51 | def get_request(*, scope: Scope, receive: Receive, uri_parser: AbstractURIParser, **kwargs) -> ConnexionRequest: # type: ignore 52 | return ConnexionRequest(scope, receive, uri_parser=uri_parser) 53 | 54 | 55 | PATH_PARAMETER = re.compile(r"\{([^}]*)\}") 56 | PATH_PARAMETER_CONVERTERS = {"integer": "int", "number": "float", "path": "path"} 57 | 58 | 59 | def convert_path_parameter(match, types): 60 | name = match.group(1) 61 | swagger_type = types.get(name) 62 | converter = PATH_PARAMETER_CONVERTERS.get(swagger_type) 63 | return f'{{{name.replace("-", "_")}{":" if converter else ""}{converter or ""}}}' 64 | 65 | 66 | def starlettify_path(swagger_path, types=None): 67 | """ 68 | Convert swagger path templates to flask path templates 69 | 70 | :type swagger_path: str 71 | :type types: dict 72 | :rtype: str 73 | 74 | >>> starlettify_path('/foo-bar/{my-param}') 75 | '/foo-bar/{my_param}' 76 | 77 | >>> starlettify_path('/foo/{someint}', {'someint': 'int'}) 78 | '/foo/{someint:int}' 79 | """ 80 | if types is None: 81 | types = {} 82 | convert_match = functools.partial(convert_path_parameter, types=types) 83 | return PATH_PARAMETER.sub(convert_match, swagger_path) 84 | 85 | 86 | class FloatConverter(starlette.convertors.FloatConvertor): 87 | """Starlette converter for OpenAPI number type""" 88 | 89 | regex = r"[+-]?[0-9]*(\.[0-9]*)?" 90 | 91 | 92 | class IntegerConverter(starlette.convertors.IntegerConvertor): 93 | """Starlette converter for OpenAPI integer type""" 94 | 95 | regex = r"[+-]?[0-9]+" 96 | -------------------------------------------------------------------------------- /connexion/handlers.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines error handlers, operations that produce proper response problems. 3 | """ 4 | 5 | import logging 6 | 7 | from .exceptions import ResolverProblem 8 | 9 | logger = logging.getLogger("connexion.handlers") 10 | 11 | RESOLVER_ERROR_ENDPOINT_RANDOM_DIGITS = 6 12 | 13 | 14 | class ResolverErrorHandler: 15 | """ 16 | Handler for responding to ResolverError. 17 | """ 18 | 19 | def __init__(self, status_code, exception): 20 | self.status_code = status_code 21 | self.exception = exception 22 | 23 | @property 24 | def function(self): 25 | return self.handle 26 | 27 | def handle(self, *args, **kwargs): 28 | raise ResolverProblem( 29 | detail=self.exception.args[0], 30 | status=self.status_code, 31 | ) 32 | 33 | @property 34 | def operation_id(self): 35 | return "noop" 36 | 37 | @property 38 | def randomize_endpoint(self): 39 | return RESOLVER_ERROR_ENDPOINT_RANDOM_DIGITS 40 | 41 | def get_path_parameter_types(self): 42 | return {} 43 | 44 | @property 45 | def uri_parser_class(self): 46 | return "dummy" 47 | 48 | @property 49 | def api(self): 50 | return "dummy" 51 | 52 | def get_mimetype(self): 53 | return "dummy" 54 | 55 | async def __call__(self, *args, **kwargs): 56 | raise ResolverProblem( 57 | detail=self.exception.args[0], 58 | status=self.status_code, 59 | ) 60 | -------------------------------------------------------------------------------- /connexion/http_facts.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains definitions of the HTTP protocol. 3 | """ 4 | 5 | FORM_CONTENT_TYPES = ["application/x-www-form-urlencoded", "multipart/form-data"] 6 | 7 | METHODS = {"get", "put", "post", "delete", "options", "head", "patch", "trace"} 8 | 9 | HTTP_STATUS_CODES = { 10 | 100: "Continue", 11 | 101: "Switching Protocols", 12 | 102: "Processing", 13 | 103: "Early Hints", # see RFC 8297 14 | 200: "OK", 15 | 201: "Created", 16 | 202: "Accepted", 17 | 203: "Non Authoritative Information", 18 | 204: "No Content", 19 | 205: "Reset Content", 20 | 206: "Partial Content", 21 | 207: "Multi Status", 22 | 208: "Already Reported", # see RFC 5842 23 | 226: "IM Used", # see RFC 3229 24 | 300: "Multiple Choices", 25 | 301: "Moved Permanently", 26 | 302: "Found", 27 | 303: "See Other", 28 | 304: "Not Modified", 29 | 305: "Use Proxy", 30 | 306: "Switch Proxy", # unused 31 | 307: "Temporary Redirect", 32 | 308: "Permanent Redirect", 33 | 400: "Bad Request", 34 | 401: "Unauthorized", 35 | 402: "Payment Required", # unused 36 | 403: "Forbidden", 37 | 404: "Not Found", 38 | 405: "Method Not Allowed", 39 | 406: "Not Acceptable", 40 | 407: "Proxy Authentication Required", 41 | 408: "Request Timeout", 42 | 409: "Conflict", 43 | 410: "Gone", 44 | 411: "Length Required", 45 | 412: "Precondition Failed", 46 | 413: "Request Entity Too Large", 47 | 414: "Request URI Too Long", 48 | 415: "Unsupported Media Type", 49 | 416: "Requested Range Not Satisfiable", 50 | 417: "Expectation Failed", 51 | 418: "I'm a teapot", # see RFC 2324 52 | 421: "Misdirected Request", # see RFC 7540 53 | 422: "Unprocessable Entity", 54 | 423: "Locked", 55 | 424: "Failed Dependency", 56 | 425: "Too Early", # see RFC 8470 57 | 426: "Upgrade Required", 58 | 428: "Precondition Required", # see RFC 6585 59 | 429: "Too Many Requests", 60 | 431: "Request Header Fields Too Large", 61 | 449: "Retry With", # proprietary MS extension 62 | 451: "Unavailable For Legal Reasons", 63 | 500: "Internal Server Error", 64 | 501: "Not Implemented", 65 | 502: "Bad Gateway", 66 | 503: "Service Unavailable", 67 | 504: "Gateway Timeout", 68 | 505: "HTTP Version Not Supported", 69 | 506: "Variant Also Negotiates", # see RFC 2295 70 | 507: "Insufficient Storage", 71 | 508: "Loop Detected", # see RFC 5842 72 | 510: "Not Extended", 73 | 511: "Network Authentication Failed", 74 | } 75 | -------------------------------------------------------------------------------- /connexion/jsonifier.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module centralizes all functionality related to json encoding and decoding in Connexion. 3 | """ 4 | 5 | import datetime 6 | import functools 7 | import json 8 | import typing as t 9 | import uuid 10 | from decimal import Decimal 11 | 12 | 13 | def wrap_default(default_fn: t.Callable) -> t.Callable: 14 | """The Connexion defaults for JSON encoding. Handles extra types compared to the 15 | built-in :class:`json.JSONEncoder`. 16 | 17 | - :class:`datetime.datetime` and :class:`datetime.date` are 18 | serialized to :rfc:`822` strings. This is the same as the HTTP 19 | date format. 20 | - :class:`decimal.Decimal` is serialized to a float. 21 | - :class:`uuid.UUID` is serialized to a string. 22 | """ 23 | 24 | @functools.wraps(default_fn) 25 | def wrapped_default(self, o): 26 | if isinstance(o, datetime.datetime): 27 | if o.tzinfo: 28 | # eg: '2015-09-25T23:14:42.588601+00:00' 29 | return o.isoformat("T") 30 | else: 31 | # No timezone present - assume UTC. 32 | # eg: '2015-09-25T23:14:42.588601Z' 33 | return o.isoformat("T") + "Z" 34 | 35 | if isinstance(o, datetime.date): 36 | return o.isoformat() 37 | 38 | if isinstance(o, Decimal): 39 | return float(o) 40 | 41 | if isinstance(o, uuid.UUID): 42 | return str(o) 43 | 44 | return default_fn(self, o) 45 | 46 | return wrapped_default 47 | 48 | 49 | class JSONEncoder(json.JSONEncoder): 50 | """The default Connexion JSON encoder. Handles extra types compared to the 51 | built-in :class:`json.JSONEncoder`. 52 | 53 | - :class:`datetime.datetime` and :class:`datetime.date` are 54 | serialized to :rfc:`822` strings. This is the same as the HTTP 55 | date format. 56 | - :class:`uuid.UUID` is serialized to a string. 57 | """ 58 | 59 | @wrap_default 60 | def default(self, o): 61 | return super().default(o) 62 | 63 | 64 | class Jsonifier: 65 | """ 66 | Central point to serialize and deserialize to/from JSon in Connexion. 67 | """ 68 | 69 | def __init__(self, json_=json, **kwargs): 70 | """ 71 | :param json_: json library to use. Must have loads() and dumps() method # NOQA 72 | :param kwargs: default arguments to pass to json.dumps() 73 | """ 74 | self.json = json_ 75 | self.dumps_args = kwargs 76 | self.dumps_args.setdefault("cls", JSONEncoder) 77 | 78 | def dumps(self, data, **kwargs): 79 | """Central point where JSON serialization happens inside 80 | Connexion. 81 | """ 82 | for k, v in self.dumps_args.items(): 83 | kwargs.setdefault(k, v) 84 | return self.json.dumps(data, **kwargs) + "\n" 85 | 86 | def loads(self, data): 87 | """Central point where JSON deserialization happens inside 88 | Connexion. 89 | """ 90 | if isinstance(data, bytes): 91 | data = data.decode() 92 | 93 | try: 94 | return self.json.loads(data) 95 | except Exception: 96 | if isinstance(data, str): 97 | return data 98 | -------------------------------------------------------------------------------- /connexion/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | from .abstract import SpecMiddleware # NOQA 2 | from .main import ConnexionMiddleware, MiddlewarePosition # NOQA 3 | -------------------------------------------------------------------------------- /connexion/middleware/context.py: -------------------------------------------------------------------------------- 1 | """The ContextMiddleware creates a global context based the scope. It should be last in the 2 | middleware stack, so it exposes the scope passed to the application""" 3 | from starlette.types import ASGIApp, Receive, Scope, Send 4 | 5 | from connexion.context import _context, _operation, _receive, _scope 6 | from connexion.middleware.abstract import RoutedAPI, RoutedMiddleware 7 | from connexion.operations import AbstractOperation 8 | 9 | 10 | class ContextOperation: 11 | def __init__( 12 | self, 13 | next_app: ASGIApp, 14 | *, 15 | operation: AbstractOperation, 16 | ) -> None: 17 | self.next_app = next_app 18 | self.operation = operation 19 | 20 | async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: 21 | _context.set(scope.get("extensions", {}).get("connexion_context", {})) 22 | _operation.set(self.operation) 23 | _receive.set(receive) 24 | _scope.set(scope) 25 | await self.next_app(scope, receive, send) 26 | 27 | 28 | class ContextAPI(RoutedAPI[ContextOperation]): 29 | def __init__(self, *args, **kwargs) -> None: 30 | super().__init__(*args, **kwargs) 31 | self.add_paths() 32 | 33 | def make_operation(self, operation: AbstractOperation) -> ContextOperation: 34 | return ContextOperation(self.next_app, operation=operation) 35 | 36 | 37 | class ContextMiddleware(RoutedMiddleware[ContextAPI]): 38 | """Middleware to expose operation specific context to application.""" 39 | 40 | api_cls = ContextAPI 41 | -------------------------------------------------------------------------------- /connexion/middleware/lifespan.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from starlette.routing import Router 4 | from starlette.types import ASGIApp, Receive, Scope, Send 5 | 6 | Lifespan = t.Callable[[t.Any], t.AsyncContextManager] 7 | 8 | 9 | class LifespanMiddleware: 10 | """ 11 | Middleware that adds support for Starlette lifespan handlers 12 | (https://www.starlette.io/lifespan/). 13 | """ 14 | 15 | def __init__(self, next_app: ASGIApp, *, lifespan: t.Optional[Lifespan]) -> None: 16 | self.next_app = next_app 17 | self._lifespan = lifespan 18 | # Leverage a Starlette Router for lifespan handling only 19 | self.router = Router(lifespan=lifespan) 20 | 21 | async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: 22 | # If no lifespan is registered, pass to next app so it can be handled downstream. 23 | if scope["type"] == "lifespan" and self._lifespan: 24 | await self.router(scope, receive, send) 25 | else: 26 | await self.next_app(scope, receive, send) 27 | -------------------------------------------------------------------------------- /connexion/middleware/server_error.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import typing as t 3 | 4 | from starlette.middleware.errors import ( 5 | ServerErrorMiddleware as StarletteServerErrorMiddleware, 6 | ) 7 | from starlette.types import ASGIApp 8 | 9 | from connexion.exceptions import InternalServerError 10 | from connexion.lifecycle import ConnexionRequest, ConnexionResponse 11 | from connexion.middleware.exceptions import connexion_wrapper 12 | from connexion.types import MaybeAwaitable 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class ServerErrorMiddleware(StarletteServerErrorMiddleware): 18 | """Subclass of starlette ServerErrorMiddleware to change handling of Unhandled Server 19 | exceptions to existing connexion behavior.""" 20 | 21 | def __init__( 22 | self, 23 | next_app: ASGIApp, 24 | handler: t.Optional[ 25 | t.Callable[[ConnexionRequest, Exception], MaybeAwaitable[ConnexionResponse]] 26 | ] = None, 27 | ): 28 | handler = connexion_wrapper(handler) if handler else None 29 | super().__init__(next_app, handler=handler) 30 | 31 | @staticmethod 32 | @connexion_wrapper 33 | def error_response(_request: ConnexionRequest, exc: Exception) -> ConnexionResponse: 34 | """Default handler for any unhandled Exception""" 35 | logger.error("%r", exc, exc_info=exc) 36 | return InternalServerError().to_problem() 37 | -------------------------------------------------------------------------------- /connexion/mock.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a mock resolver that returns mock functions for operations it cannot resolve. 3 | """ 4 | 5 | import functools 6 | import logging 7 | 8 | from connexion.resolver import Resolution, Resolver, ResolverError 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class MockResolver(Resolver): 14 | def __init__(self, mock_all): 15 | super().__init__() 16 | self.mock_all = mock_all 17 | self._operation_id_counter = 1 18 | 19 | def resolve(self, operation): 20 | """ 21 | Mock operation resolver 22 | 23 | :type operation: connexion.operations.AbstractOperation 24 | """ 25 | operation_id = self.resolve_operation_id(operation) 26 | if not operation_id: 27 | # just generate an unique operation ID 28 | operation_id = f"mock-{self._operation_id_counter}" 29 | self._operation_id_counter += 1 30 | 31 | mock_func = functools.partial(self.mock_operation, operation=operation) 32 | if self.mock_all: 33 | func = mock_func 34 | else: 35 | try: 36 | func = self.resolve_function_from_operation_id(operation_id) 37 | msg = "... Successfully resolved operationId '{}'! Mock is *not* used for this operation.".format( 38 | operation_id 39 | ) 40 | logger.debug(msg) 41 | except ResolverError as resolution_error: 42 | logger.debug( 43 | "... {}! Mock function is used for this operation.".format( 44 | resolution_error.args[0].capitalize() 45 | ) 46 | ) 47 | func = mock_func 48 | return Resolution(func, operation_id) 49 | 50 | def mock_operation(self, operation, *args, **kwargs): 51 | resp, code = operation.example_response() 52 | if resp is not None: 53 | return resp, code 54 | return ( 55 | "No example response defined in the API, and response " 56 | "auto-generation disabled. To enable response auto-generation, " 57 | "install connexion using the mock extra (connexion[mock])", 58 | 501, 59 | ) 60 | -------------------------------------------------------------------------------- /connexion/operations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines Connexion Operation classes. A Connexion Operation implements an OpenAPI 3 | operation, which describes a single API operation on a path. It wraps the view function linked to 4 | the operation with decorators to handle security, validation, serialization etc. based on the 5 | OpenAPI specification, and exposes the result to be registered as a route on the application. 6 | 7 | """ 8 | 9 | from .abstract import AbstractOperation # noqa 10 | from .openapi import OpenAPIOperation # noqa 11 | from .swagger2 import Swagger2Operation # noqa 12 | -------------------------------------------------------------------------------- /connexion/options.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines a Connexion specific options class to pass to the Connexion App or API. 3 | """ 4 | import dataclasses 5 | import logging 6 | import typing as t 7 | 8 | try: 9 | from swagger_ui_bundle import swagger_ui_path as default_template_dir 10 | except ImportError: 11 | default_template_dir = None 12 | 13 | NO_UI_MSG = """The swagger_ui directory could not be found. 14 | Please install connexion with extra install: pip install connexion[swagger-ui] 15 | or provide the path to your local installation by passing swagger_path= 16 | """ 17 | 18 | logger = logging.getLogger("connexion.options") 19 | 20 | 21 | @dataclasses.dataclass 22 | class SwaggerUIOptions: 23 | """Options to configure the Swagger UI. 24 | 25 | :param serve_spec: Whether to serve the Swagger / OpenAPI Specification 26 | :param spec_path: Where to serve the Swagger / OpenAPI Specification 27 | 28 | :param swagger_ui: Whether to serve the Swagger UI 29 | :param swagger_ui_path: Where to serve the Swagger UI 30 | :param swagger_ui_config: Options to configure the Swagger UI. See 31 | https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration 32 | for an overview of the available options. 33 | :param swagger_ui_template_dir: Directory with static files to use to serve Swagger UI 34 | :param swagger_ui_template_arguments: Arguments passed to the Swagger UI template. Useful 35 | when providing your own template dir with additional template arguments. 36 | """ 37 | 38 | serve_spec: bool = True 39 | spec_path: t.Optional[str] = None 40 | 41 | swagger_ui: bool = True 42 | swagger_ui_config: dict = dataclasses.field(default_factory=dict) 43 | swagger_ui_path: str = "/ui" 44 | swagger_ui_template_dir: t.Optional[str] = None 45 | swagger_ui_template_arguments: dict = dataclasses.field(default_factory=dict) 46 | 47 | 48 | class SwaggerUIConfig: 49 | """Class holding swagger UI specific options.""" 50 | 51 | def __init__( 52 | self, 53 | options: t.Optional[SwaggerUIOptions] = None, 54 | oas_version: t.Tuple[int, ...] = (2,), 55 | ): 56 | if oas_version >= (3, 0, 0): 57 | self.spec_path = "/openapi.json" 58 | else: 59 | self.spec_path = "/swagger.json" 60 | 61 | if options is not None and not isinstance(options, SwaggerUIOptions): 62 | raise ValueError( 63 | f"`swaggger_ui_options` should be of type `SwaggerUIOptions`, " 64 | f"but received {type(options)} instead." 65 | ) 66 | 67 | self._options = options or SwaggerUIOptions() 68 | 69 | @property 70 | def openapi_spec_available(self) -> bool: 71 | """Whether to make the OpenAPI Specification available.""" 72 | return self._options.serve_spec 73 | 74 | @property 75 | def openapi_spec_path(self) -> str: 76 | """Path to host the Swagger UI.""" 77 | return self._options.spec_path or self.spec_path 78 | 79 | @property 80 | def swagger_ui_available(self) -> bool: 81 | """Whether to make the Swagger UI available.""" 82 | if self._options.swagger_ui and self.swagger_ui_template_dir is None: 83 | logger.warning(NO_UI_MSG) 84 | return False 85 | return self._options.swagger_ui 86 | 87 | @property 88 | def swagger_ui_path(self) -> str: 89 | """Path to mount the Swagger UI and make it accessible via a browser.""" 90 | return self._options.swagger_ui_path 91 | 92 | @property 93 | def swagger_ui_template_dir(self) -> str: 94 | """Directory with static files to use to serve Swagger UI.""" 95 | return self._options.swagger_ui_template_dir or default_template_dir 96 | 97 | @property 98 | def swagger_ui_config(self) -> dict: 99 | """Options to configure the Swagger UI.""" 100 | return self._options.swagger_ui_config 101 | 102 | @property 103 | def swagger_ui_template_arguments(self) -> dict: 104 | """Arguments passed to the Swagger UI template.""" 105 | return self._options.swagger_ui_template_arguments 106 | -------------------------------------------------------------------------------- /connexion/problem.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains a Python interface for Problem Details for HTTP APIs 3 | , which is a standardized format 4 | to communicate distinct "problem types" to non-human consumers. 5 | """ 6 | 7 | import json 8 | 9 | 10 | def problem(status, title, detail, type=None, instance=None, headers=None, ext=None): 11 | """ 12 | Returns a `Problem Details `_ error response. 13 | 14 | 15 | :param status: The HTTP status code generated by the origin server for this occurrence of the problem. 16 | :type status: int 17 | :param title: A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to 18 | occurrence of the problem, except for purposes of localisation. 19 | :type title: str 20 | :param detail: An human readable explanation specific to this occurrence of the problem. 21 | :type detail: str 22 | :param type: An absolute URI that identifies the problem type. When dereferenced, it SHOULD provide human-readable 23 | documentation for the problem type (e.g., using HTML). When this member is not present its value is 24 | assumed to be "about:blank". 25 | :type: type: str 26 | :param instance: An absolute URI that identifies the specific occurrence of the problem. It may or may not yield 27 | further information if dereferenced. 28 | :type instance: str 29 | :param headers: HTTP headers to include in the response 30 | :type headers: dict | None 31 | :param ext: Extension members to include in the body 32 | :type ext: dict | None 33 | :return: error response 34 | :rtype: ConnexionResponse 35 | """ 36 | from .lifecycle import ConnexionResponse # prevent circular import 37 | 38 | if not type: 39 | type = "about:blank" 40 | 41 | problem_response = { 42 | "type": type, 43 | "title": title, 44 | "detail": detail, 45 | "status": status, 46 | } 47 | if instance: 48 | problem_response["instance"] = instance 49 | if ext: 50 | problem_response.update(ext) 51 | 52 | mimetype = content_type = "application/problem+json" 53 | 54 | return ConnexionResponse( 55 | status, 56 | mimetype, 57 | content_type, 58 | body=json.dumps(problem_response), 59 | headers=headers, 60 | ) 61 | -------------------------------------------------------------------------------- /connexion/testing.py: -------------------------------------------------------------------------------- 1 | import contextvars 2 | import typing as t 3 | from unittest.mock import MagicMock 4 | 5 | from starlette.types import Receive, Scope 6 | 7 | from connexion.context import _context, _operation, _receive, _scope 8 | from connexion.operations import AbstractOperation 9 | 10 | 11 | class TestContext: 12 | __test__ = False # Pytest 13 | 14 | def __init__( 15 | self, 16 | *, 17 | context: t.Optional[dict] = None, 18 | operation: t.Optional[AbstractOperation] = None, 19 | receive: t.Optional[Receive] = None, 20 | scope: t.Optional[Scope] = None, 21 | ) -> None: 22 | self.context = context if context is not None else self.build_context() 23 | self.operation = operation if operation is not None else self.build_operation() 24 | self.receive = receive if receive is not None else self.build_receive() 25 | self.scope = scope if scope is not None else self.build_scope() 26 | 27 | self.tokens: t.Dict[str, contextvars.Token] = {} 28 | 29 | def __enter__(self) -> None: 30 | self.tokens["context"] = _context.set(self.context) 31 | self.tokens["operation"] = _operation.set(self.operation) 32 | self.tokens["receive"] = _receive.set(self.receive) 33 | self.tokens["scope"] = _scope.set(self.scope) 34 | return 35 | 36 | def __exit__(self, type, value, traceback): 37 | _context.reset(self.tokens["context"]) 38 | _operation.reset(self.tokens["operation"]) 39 | _receive.reset(self.tokens["receive"]) 40 | _scope.reset(self.tokens["scope"]) 41 | return False 42 | 43 | @staticmethod 44 | def build_context() -> dict: 45 | return {} 46 | 47 | @staticmethod 48 | def build_operation() -> AbstractOperation: 49 | return MagicMock(name="operation") 50 | 51 | @staticmethod 52 | def build_receive() -> Receive: 53 | async def receive() -> t.MutableMapping[str, t.Any]: 54 | return { 55 | "type": "http.request", 56 | "body": b"", 57 | } 58 | 59 | return receive 60 | 61 | @staticmethod 62 | def build_scope(**kwargs) -> Scope: 63 | scope = { 64 | "type": "http", 65 | "query_string": b"", 66 | "headers": [(b"Content-Type", b"application/octet-stream")], 67 | } 68 | 69 | for key, value in kwargs.items(): 70 | scope[key] = value 71 | 72 | return scope 73 | -------------------------------------------------------------------------------- /connexion/types.py: -------------------------------------------------------------------------------- 1 | import types 2 | import typing as t 3 | 4 | # Maybe Awaitable 5 | _ReturnType = t.TypeVar("_ReturnType") 6 | MaybeAwaitable = t.Union[t.Awaitable[_ReturnType], _ReturnType] 7 | 8 | # WSGIApp 9 | Environ = t.Mapping[str, object] 10 | 11 | _WriteCallable = t.Callable[[bytes], t.Any] 12 | _ExcInfo = t.Tuple[type, BaseException, types.TracebackType] 13 | 14 | _StartResponseCallable = t.Callable[ 15 | [ 16 | str, # status 17 | t.Sequence[t.Tuple[str, str]], # response headers 18 | ], 19 | _WriteCallable, # write() callable 20 | ] 21 | _StartResponseCallableWithExcInfo = t.Callable[ 22 | [ 23 | str, # status 24 | t.Sequence[t.Tuple[str, str]], # response headers 25 | t.Optional[_ExcInfo], # exc_info 26 | ], 27 | _WriteCallable, # write() callable 28 | ] 29 | StartResponse = t.Union[_StartResponseCallable, _StartResponseCallableWithExcInfo] 30 | ResponseStream = t.Iterable[bytes] 31 | 32 | WSGIApp = t.Callable[[Environ, StartResponse], ResponseStream] 33 | -------------------------------------------------------------------------------- /connexion/validators/__init__.py: -------------------------------------------------------------------------------- 1 | from connexion.datastructures import MediaTypeDict 2 | 3 | from .abstract import ( # NOQA 4 | AbstractRequestBodyValidator, 5 | AbstractResponseBodyValidator, 6 | ) 7 | from .form_data import FormDataValidator, MultiPartFormDataValidator 8 | from .json import DefaultsJSONRequestBodyValidator # NOQA 9 | from .json import ( 10 | JSONRequestBodyValidator, 11 | JSONResponseBodyValidator, 12 | TextResponseBodyValidator, 13 | ) 14 | from .parameter import ParameterValidator 15 | 16 | VALIDATOR_MAP = { 17 | "parameter": ParameterValidator, 18 | "body": MediaTypeDict( 19 | { 20 | "*/*json": JSONRequestBodyValidator, 21 | "application/x-www-form-urlencoded": FormDataValidator, 22 | "multipart/form-data": MultiPartFormDataValidator, 23 | } 24 | ), 25 | "response": MediaTypeDict( 26 | { 27 | "*/*json": JSONResponseBodyValidator, 28 | "text/plain": TextResponseBodyValidator, 29 | } 30 | ), 31 | } 32 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _* 2 | -------------------------------------------------------------------------------- /docs/_static/css/default.css: -------------------------------------------------------------------------------- 1 | .rst-content .code-block-caption { 2 | text-align: left; 3 | padding: 0px 0px 5px 5px; 4 | } 5 | -------------------------------------------------------------------------------- /docs/_static/js/guru_widget.js: -------------------------------------------------------------------------------- 1 | document.write(` 2 | 9 | `); -------------------------------------------------------------------------------- /docs/cli.rst: -------------------------------------------------------------------------------- 1 | Command-Line Interface 2 | ====================== 3 | For convenience Connexion provides a command-line interface 4 | (CLI). This interface aims to be a starting point in developing or 5 | testing OpenAPI specifications with Connexion. 6 | 7 | Running an OpenAPI specification 8 | -------------------------------- 9 | 10 | The subcommand ``run`` of Connexion's CLI makes it easy to run OpenAPI 11 | specifications directly even before any operation handler function gets 12 | implemented. This allows you to verify and inspect how your API will 13 | work with Connexion. 14 | 15 | To run your specification, execute in your shell: 16 | 17 | .. code-block:: bash 18 | 19 | $ connexion run your_api.yaml --stub 20 | 21 | This command will tell Connexion to run the ``your_api.yaml`` 22 | specification file attaching a stub operation (``--stub``) to the 23 | unavailable operations/functions of your API, which will return a ``501 Not Implemented`` response. 24 | 25 | The basic usage of this command is: 26 | 27 | .. code-block:: bash 28 | 29 | $ connexion run [OPTIONS] SPEC_FILE [BASE_MODULE_PATH] 30 | 31 | Where: 32 | 33 | - SPEC_FILE: Your OpenAPI specification file in YAML format. Can also be given 34 | as a URL, which will be automatically downloaded. 35 | - BASE_MODULE_PATH (optional): filesystem path where the API endpoints 36 | handlers are going to be imported from. In short, where your Python 37 | code is saved. 38 | 39 | There are more options available for the ``run`` command, for a full 40 | list run: 41 | 42 | .. code-block:: bash 43 | 44 | $ connexion run --help 45 | 46 | Running a mock server 47 | --------------------- 48 | 49 | You can run a simple server which returns example responses on every request. 50 | 51 | The example responses can be defined in the ``examples`` response property of 52 | the OpenAPI specification. If no examples are specified, and you have installed connexion with the `mock` extra (`pip install connexion[mock]`), an example is generated based on the provided schema. 53 | 54 | Your API specification file is not required to have any ``operationId``. 55 | 56 | .. code-block:: bash 57 | 58 | $ connexion run your_api.yaml --mock=all 59 | 60 | $ connexion run https://raw.githubusercontent.com/spec-first/connexion/main/examples/helloworld_async/spec/openapi.yaml --mock=all 61 | -------------------------------------------------------------------------------- /docs/context.rst: -------------------------------------------------------------------------------- 1 | Context 2 | ======= 3 | 4 | The ``ContextMiddleware`` included in Connexion provides some information about the current request 5 | context as thread-safe request-level global variables. 6 | 7 | You can access them by importing them from ``connexion.context``: 8 | 9 | .. code-block:: python 10 | 11 | from connexion.context import context, operation, receive, request, scope 12 | from connexion import request # alias for connexion.context.request 13 | 14 | Note that when trying to access these context variables outside of the request handling flow, or 15 | without running the ``ContextMiddleware``, the following ``RuntimeError`` will be thrown: 16 | 17 | .. code-block:: text 18 | 19 | RuntimeError: Working outside of operation context. Make sure your app is wrapped in a 20 | ContextMiddleware and you're processing a request while accessing the context. 21 | 22 | See below for an explanation of the different variables. 23 | 24 | request 25 | ------- 26 | 27 | A ``Request`` object representing the incoming request. This is an instance of the 28 | ``ConnexionRequest``. 29 | 30 | .. dropdown:: View a detailed reference of the ``ConnexionRequest`` class 31 | :icon: eye 32 | 33 | .. autoclass:: connexion.lifecycle.ConnexionRequest 34 | :noindex: 35 | :members: 36 | :undoc-members: 37 | :inherited-members: 38 | 39 | Some of the methods and attributes are coroutines that can only be accessed from an ``async`` 40 | context. When using the ``FlaskApp``, you might want to import the Flask request instead: 41 | 42 | .. code-block:: python 43 | 44 | from flask import request 45 | 46 | operation 47 | --------- 48 | 49 | An ``Operation`` object representing the matched operation from your OpenAPI specification. 50 | 51 | .. tab-set:: 52 | 53 | .. tab-item:: OpenAPI 3 54 | :sync: OpenAPI 3 55 | 56 | When using OpenAPI 3, this is an instance of the ``OpenAPIOperation`` class. 57 | 58 | .. dropdown:: View a detailed reference of the ``OpenAPIOperation`` class 59 | :icon: eye 60 | 61 | .. autoclass:: connexion.operations.OpenAPIOperation 62 | :members: 63 | :undoc-members: 64 | :inherited-members: 65 | 66 | .. tab-item:: Swagger 2 67 | :sync: Swagger 2 68 | 69 | When using Swagger 2, this is an instance of the ``Swagger2Operation`` class. 70 | 71 | .. dropdown:: View a detailed reference of the ``Swagger2Operation`` class 72 | :icon: eye 73 | 74 | .. autoclass:: connexion.operations.Swagger2Operation 75 | :members: 76 | :undoc-members: 77 | :inherited-members: 78 | 79 | scope 80 | ----- 81 | 82 | The ASGI scope as received by the ``ContextMiddleware``, thus containing any changes propagated by 83 | upstream middleware. The ASGI scope is presented as a ``dict``. Please refer to the `ASGI spec`_ 84 | for more information on its contents. 85 | 86 | context.context 87 | --------------- 88 | 89 | A dict containing the information from the security middleware: 90 | 91 | .. code-block:: python 92 | 93 | { 94 | "user": ... # User information from authentication 95 | "token_info": ... # Token information from authentication 96 | } 97 | 98 | Third party or custom middleware might add additional fields to this. 99 | 100 | receive 101 | ------- 102 | 103 | .. warning:: Advanced usage 104 | 105 | The receive channel as received by the ``ContextMiddleware``. Note that the receive channel might 106 | already be read by other parts of Connexion (eg. when accessing the body via the ``Request``, or 107 | when it is injected into your Python function), and that reading it yourself might make it 108 | unavailable for those parts of the application. 109 | 110 | The receive channel can only be accessed from an ``async`` context and is therefore not relevant 111 | when using the ``FlaskApp``. 112 | 113 | .. _ASGI spec: https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope -------------------------------------------------------------------------------- /docs/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spec-first/connexion/dd79c1146ae31be2145e224552dea95a7473e1fa/docs/images/architecture.png -------------------------------------------------------------------------------- /docs/images/sponsors/Fern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spec-first/connexion/dd79c1146ae31be2145e224552dea95a7473e1fa/docs/images/sponsors/Fern.png -------------------------------------------------------------------------------- /docs/images/sponsors/ML6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spec-first/connexion/dd79c1146ae31be2145e224552dea95a7473e1fa/docs/images/sponsors/ML6.png -------------------------------------------------------------------------------- /docs/images/swagger_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spec-first/connexion/dd79c1146ae31be2145e224552dea95a7473e1fa/docs/images/swagger_ui.png -------------------------------------------------------------------------------- /docs/lifespan.rst: -------------------------------------------------------------------------------- 1 | Lifespan 2 | ======== 3 | 4 | You can register lifespan handlers to run code before the app starts, or after it shuts down. 5 | This ideal for setting up and tearing down database connections or machine learning models for 6 | instance. 7 | 8 | .. tab-set:: 9 | 10 | .. tab-item:: AsyncApp 11 | :sync: AsyncApp 12 | 13 | .. code-block:: python 14 | 15 | import contextlib 16 | import typing 17 | 18 | from connexion import AsyncApp, ConnexionMiddleware, request 19 | 20 | @contextlib.asynccontextmanager 21 | async def lifespan_handler(app: ConnexionMiddleware) -> typing.AsyncIterator: 22 | """Called at startup and shutdown, can yield state which will be available on the 23 | request.""" 24 | client = Client() 25 | yield {"client": client} 26 | client.close() 27 | 28 | def route(): 29 | """Endpoint function called when receiving a request, you can access the state 30 | on the request here.""" 31 | client = request.state.client 32 | client.call() 33 | 34 | app = AsyncApp(__name__, lifespan=lifespan_handler) 35 | 36 | .. tab-item:: FlaskApp 37 | :sync: FlaskApp 38 | 39 | .. code-block:: python 40 | 41 | import contextlib 42 | import typing 43 | 44 | from connexion import FlaskApp, ConnexionMiddleware, request 45 | 46 | @contextlib.asynccontextmanager 47 | async def lifespan_handler(app: ConnexionMiddleware) -> typing.AsyncIterator: 48 | """Called at startup and shutdown, can yield state which will be available on the 49 | request.""" 50 | client = Client() 51 | yield {"client": client} 52 | client.close() 53 | 54 | def route(): 55 | """Endpoint function called when receiving a request, you can access the state 56 | on the request here.""" 57 | client = request.state.client 58 | client.call() 59 | 60 | app = FlaskApp(__name__, lifespan=lifespan_handler) 61 | 62 | .. tab-item:: ConnexionMiddleware 63 | :sync: ConnexionMiddleware 64 | 65 | .. code-block:: python 66 | 67 | import contextlib 68 | import typing 69 | 70 | from asgi_framework import App 71 | from connexion import ConnexionMiddleware, request 72 | 73 | @contextlib.asynccontextmanager 74 | async def lifespan_handler(app: ConnexionMiddleware) -> typing.AsyncIterator: 75 | """Called at startup and shutdown, can yield state which will be available on the 76 | request.""" 77 | client = Client() 78 | yield {"client": client} 79 | client.close() 80 | 81 | def endpoint(): 82 | """Endpoint function called when receiving a request, you can access the state 83 | on the request here.""" 84 | client = request.state.client 85 | client.call() 86 | 87 | app = App(__name__) 88 | app = ConnexionMiddleware(app, lifespan=lifespan_handler) 89 | 90 | Running lifespan in tests 91 | ------------------------- 92 | 93 | If you want lifespan handlers to be called during tests, you can use the ``test_client`` as a 94 | context manager. 95 | 96 | .. code-block:: python 97 | 98 | def test_homepage(): 99 | app = ... # Set up app 100 | with app.test_client() as client: 101 | # Application's lifespan is called on entering the block. 102 | response = client.get("/") 103 | assert response.status_code == 200 104 | 105 | # And the lifespan's teardown is run when exiting the block. 106 | 107 | For more information, please refer to the `Starlette documentation`_. 108 | 109 | .. _Starlette documentation: https://www.starlette.io/lifespan/ 110 | -------------------------------------------------------------------------------- /docs/swagger_ui.rst: -------------------------------------------------------------------------------- 1 | The Swagger UI 2 | ============== 3 | 4 | If you installed connexion using the :code:`swagger-ui` extra, a Swagger UI is available for each 5 | API, providing interactive documentation. By default the UI is hosted at :code:`{base_path}/ui/` 6 | where :code:`base_path`` is the base path of the API. 7 | 8 | **https://{host}/{base_path}/ui/** 9 | 10 | .. image:: images/swagger_ui.png 11 | 12 | Configuring the Swagger UI 13 | -------------------------- 14 | 15 | You can change this path through the ``swagger_ui_options`` argument, either whe instantiating 16 | your application, or when adding your api: 17 | 18 | 19 | .. tab-set:: 20 | 21 | .. tab-item:: AsyncApp 22 | :sync: AsyncApp 23 | 24 | .. code-block:: python 25 | :caption: **app.py** 26 | 27 | from connexion import AsyncApp 28 | from connexion.options import SwaggerUIOptions 29 | 30 | options = SwaggerUIOptions(swagger_ui_path="/docs") 31 | 32 | app = AsyncApp(__name__, swagger_ui_options=options) 33 | app.add_api("openapi.yaml", swagger_ui_options=options) 34 | 35 | .. tab-item:: FlaskApp 36 | :sync: FlaskApp 37 | 38 | .. code-block:: python 39 | :caption: **app.py** 40 | 41 | from connexion import FlaskApp 42 | from connexion.options import SwaggerUIOptions 43 | 44 | options = SwaggerUIOptions(swagger_ui_path="/docs") 45 | 46 | app = FlaskApp(__name__, swagger_ui_options=options) 47 | app.add_api("openapi.yaml", swagger_ui_options=options) 48 | 49 | .. tab-item:: ConnexionMiddleware 50 | :sync: ConnexionMiddleware 51 | 52 | .. code-block:: python 53 | :caption: **app.py** 54 | 55 | from asgi_framework import App 56 | from connexion import ConnexionMiddleware 57 | from connexion.options import SwaggerUIOptions 58 | 59 | options = SwaggerUIOptions(swagger_ui_path="/docs") 60 | 61 | app = App(__name__) 62 | app = ConnexionMiddleware(app, swagger_ui_options=options) 63 | app.add_api("openapi.yaml", swagger_ui_options=options): 64 | 65 | For a description of all available options, check the :class:`.SwaggerUIOptions` 66 | class. 67 | 68 | .. dropdown:: View a detailed reference of the :code:`SwaggerUIOptions` class 69 | :icon: eye 70 | 71 | .. autoclass:: connexion.options.SwaggerUIOptions 72 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | test_client 5 | ----------- 6 | 7 | Connexion exposes a ``test_client`` which you can use to make requests against your 8 | Connexion application during tests. 9 | 10 | .. code-block:: python 11 | 12 | def test_homepage(): 13 | app = ... # Set up app 14 | kwarg = {...} 15 | with app.test_client(**kwargs) as client: 16 | response = client.get("/") 17 | assert response.status_code == 200 18 | 19 | 20 | The passed in keywords used to create a `Starlette` ``TestClient`` which is then returned. 21 | 22 | For more information, please check the `Starlette documentation`_. 23 | 24 | .. _Starlette documentation: https://www.starlette.io/testclient/ 25 | 26 | TestContext 27 | ----------- 28 | 29 | To have access to the :doc:`context` variables during tests, you can use the :class:`.TestContext` 30 | provided by Connexion. 31 | 32 | .. code-block:: python 33 | 34 | from unittest.mock import MagicMock 35 | 36 | from connexion.context import operation 37 | from connexion.testing import TestContext 38 | 39 | 40 | def get_method(): 41 | """Function called within TestContext you can access the context variables here.""" 42 | return operation.method 43 | 44 | def test(): 45 | operation = MagicMock(name="operation") 46 | operation.method = "post" 47 | with TestContext(operation=operation): 48 | assert get_method() == "post 49 | 50 | If you don't pass in a certain context variable, the `TestContext` will generate a dummy one. 51 | -------------------------------------------------------------------------------- /examples/apikey/README.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | API Key Example 3 | =============== 4 | 5 | Preparing 6 | --------- 7 | 8 | Create a new virtual environment and install the required libraries 9 | with these commands: 10 | 11 | .. code-block:: bash 12 | 13 | $ python -m venv my-venv 14 | $ source my-venv/bin/activate 15 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 16 | 17 | Running 18 | ------- 19 | 20 | Launch the connexion server with this command: 21 | 22 | .. code-block:: bash 23 | 24 | $ python app.py 25 | 26 | Now open your browser and view the Swagger UI for these specification files: 27 | 28 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 29 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 30 | 31 | The hardcoded apikey is `asdf1234567890`. 32 | 33 | Test it out (in another terminal): 34 | 35 | .. code-block:: bash 36 | 37 | $ curl -H 'X-Auth: asdf1234567890' http://localhost:8080/openapi/secret 38 | $ curl -H 'X-Auth: asdf1234567890' http://localhost:8080/swagger/secret 39 | -------------------------------------------------------------------------------- /examples/apikey/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic example of a resource server 3 | """ 4 | from pathlib import Path 5 | 6 | import connexion 7 | from connexion.exceptions import OAuthProblem 8 | 9 | TOKEN_DB = {"asdf1234567890": {"uid": 100}} 10 | 11 | 12 | def apikey_auth(token, required_scopes): 13 | info = TOKEN_DB.get(token, None) 14 | 15 | if not info: 16 | raise OAuthProblem("Invalid token") 17 | 18 | return info 19 | 20 | 21 | def get_secret(user) -> str: 22 | return f"You are {user} and the secret is 'wbevuec'" 23 | 24 | 25 | app = connexion.FlaskApp(__name__, specification_dir="spec") 26 | app.add_api("openapi.yaml") 27 | app.add_api("swagger.yaml") 28 | 29 | 30 | if __name__ == "__main__": 31 | app.run(f"{Path(__file__).stem}:app", port=8080) 32 | -------------------------------------------------------------------------------- /examples/apikey/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: API Key Example 4 | version: '1.0' 5 | servers: 6 | - url: /openapi 7 | paths: 8 | /secret: 9 | get: 10 | summary: Return secret string 11 | operationId: app.get_secret 12 | responses: 13 | '200': 14 | description: secret response 15 | content: 16 | '*/*': 17 | schema: 18 | type: string 19 | security: 20 | - api_key: [] 21 | components: 22 | securitySchemes: 23 | api_key: 24 | type: apiKey 25 | name: X-Auth 26 | in: header 27 | x-apikeyInfoFunc: app.apikey_auth 28 | -------------------------------------------------------------------------------- /examples/apikey/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: API Key Example 4 | version: '1.0' 5 | basePath: /swagger 6 | paths: 7 | /secret: 8 | get: 9 | summary: Return secret string 10 | operationId: app.get_secret 11 | responses: 12 | '200': 13 | description: secret response 14 | schema: 15 | type: string 16 | security: 17 | - api_key: [] 18 | securityDefinitions: 19 | api_key: 20 | type: apiKey 21 | name: X-Auth 22 | in: header 23 | x-apikeyInfoFunc: app.apikey_auth 24 | -------------------------------------------------------------------------------- /examples/basicauth/README.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | HTTP Basic Auth Example 3 | ======================= 4 | 5 | Preparing 6 | --------- 7 | 8 | Create a new virtual environment and install the required libraries 9 | with these commands: 10 | 11 | .. code-block:: bash 12 | 13 | $ python -m venv my-venv 14 | $ source my-venv/bin/activate 15 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 16 | 17 | Running 18 | ------- 19 | 20 | Launch the connexion server with this command: 21 | 22 | .. code-block:: bash 23 | 24 | $ python app.py 25 | 26 | Now open your browser and view the Swagger UI for these specification files: 27 | 28 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 29 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 30 | 31 | The hardcoded credentials are ``admin:secret`` and ``foo:bar``. 32 | -------------------------------------------------------------------------------- /examples/basicauth/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic example of a resource server 3 | """ 4 | from pathlib import Path 5 | 6 | import connexion 7 | 8 | PASSWD = {"admin": "secret", "foo": "bar"} 9 | 10 | 11 | def basic_auth(username, password): 12 | if PASSWD.get(username) == password: 13 | return {"sub": username} 14 | # optional: raise exception for custom error response 15 | return None 16 | 17 | 18 | def get_secret(user) -> str: 19 | return f"You are {user} and the secret is 'wbevuec'" 20 | 21 | 22 | app = connexion.FlaskApp(__name__, specification_dir="spec") 23 | app.add_api("openapi.yaml") 24 | app.add_api("swagger.yaml") 25 | 26 | 27 | if __name__ == "__main__": 28 | app.run(f"{Path(__file__).stem}:app", port=8080) 29 | -------------------------------------------------------------------------------- /examples/basicauth/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Basic Auth Example 4 | version: '1.0' 5 | servers: 6 | - url: /openapi 7 | paths: 8 | /secret: 9 | get: 10 | summary: Return secret string 11 | operationId: app.get_secret 12 | responses: 13 | '200': 14 | description: secret response 15 | content: 16 | '*/*': 17 | schema: 18 | type: string 19 | security: 20 | - basic: [] 21 | components: 22 | securitySchemes: 23 | basic: 24 | type: http 25 | scheme: basic 26 | x-basicInfoFunc: app.basic_auth 27 | -------------------------------------------------------------------------------- /examples/basicauth/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: Basic Auth Example 4 | version: "1.0" 5 | basePath: /swagger 6 | paths: 7 | /secret: 8 | get: 9 | summary: Return secret string 10 | operationId: app.get_secret 11 | responses: 12 | '200': 13 | description: secret response 14 | schema: 15 | type: string 16 | security: 17 | - basic: [] 18 | securityDefinitions: 19 | basic: 20 | type: basic 21 | x-basicInfoFunc: app.basic_auth 22 | -------------------------------------------------------------------------------- /examples/enforcedefaults/README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Custom Validator Example 3 | ======================== 4 | 5 | .. warning:: 6 | 7 | This example is outdated. Currently validation no longer adapts the body. 8 | TODO: decide if validation should adapt body or how we want to enable defaults otherwise. 9 | 10 | In this example we fill-in non-provided properties with their defaults. 11 | Validator code is based on example from `python-jsonschema docs`_. 12 | 13 | Preparing 14 | --------- 15 | 16 | Create a new virtual environment and install the required libraries 17 | with these commands: 18 | 19 | .. code-block:: bash 20 | 21 | $ python -m venv my-venv 22 | $ source my-venv/bin/activate 23 | $ pip install 'connexion[swagger-ui,uvicorn]>=3.1.0' 24 | 25 | Running 26 | ------- 27 | 28 | Launch the connexion server with this command: 29 | 30 | .. code-block:: bash 31 | 32 | $ python app.py 33 | 34 | Now open your browser and view the Swagger UI for these specification files: 35 | 36 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 37 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 38 | 39 | If you send a ``POST`` request with empty body ``{}``, you should receive 40 | echo with defaults filled-in. 41 | 42 | .. _python-jsonschema docs: https://python-jsonschema.readthedocs.io/en/latest/faq/#why-doesn-t-my-schema-that-has-a-default-property-actually-set-the-default-on-my-instance 43 | -------------------------------------------------------------------------------- /examples/enforcedefaults/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import connexion 4 | from connexion.validators import DefaultsJSONRequestBodyValidator 5 | 6 | 7 | def echo(data): 8 | return data 9 | 10 | 11 | validator_map = {"body": {"application/json": DefaultsJSONRequestBodyValidator}} 12 | 13 | 14 | app = connexion.AsyncApp(__name__, specification_dir="spec") 15 | app.add_api("openapi.yaml", validator_map=validator_map) 16 | app.add_api("swagger.yaml", validator_map=validator_map) 17 | 18 | 19 | if __name__ == "__main__": 20 | app.run(f"{Path(__file__).stem}:app", port=8080) 21 | -------------------------------------------------------------------------------- /examples/enforcedefaults/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | version: '1' 4 | title: Custom Validator Example 5 | servers: 6 | - url: '/openapi' 7 | 8 | paths: 9 | /echo: 10 | post: 11 | description: Echo passed data 12 | operationId: app.echo 13 | requestBody: 14 | x-body-name: data 15 | required: true 16 | content: 17 | application/json: 18 | schema: 19 | $ref: '#/components/schemas/Data' 20 | responses: 21 | '200': 22 | description: Data with defaults filled in by validator 23 | content: 24 | application/json: 25 | schema: 26 | $ref: '#/components/schemas/Data' 27 | default: 28 | description: Unexpected error 29 | content: 30 | application/json: 31 | schema: 32 | $ref: '#/components/schemas/Error' 33 | components: 34 | schemas: 35 | Data: 36 | type: object 37 | properties: 38 | outer-object: 39 | type: object 40 | default: {} 41 | properties: 42 | inner-object: 43 | type: string 44 | default: foo 45 | Error: 46 | type: string 47 | -------------------------------------------------------------------------------- /examples/enforcedefaults/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | version: '1' 4 | title: Custom Validator Example 5 | basePath: '/swagger' 6 | consumes: 7 | - application/json 8 | produces: 9 | - application/json 10 | paths: 11 | /echo: 12 | post: 13 | description: Echo passed data 14 | operationId: app.echo 15 | parameters: 16 | - name: data 17 | in: body 18 | required: true 19 | schema: 20 | $ref: '#/definitions/Data' 21 | responses: 22 | '200': 23 | description: Data with defaults filled in by validator 24 | schema: 25 | $ref: '#/definitions/Data' 26 | default: 27 | description: Unexpected error 28 | schema: 29 | $ref: '#/definitions/Error' 30 | definitions: 31 | Data: 32 | type: object 33 | properties: 34 | outer-object: 35 | type: object 36 | default: {} 37 | properties: 38 | inner-object: 39 | type: string 40 | default: foo 41 | Error: 42 | type: string 43 | -------------------------------------------------------------------------------- /examples/frameworks/README.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | Framework Examples 3 | ================== 4 | 5 | This directory contains minimal examples on how to use Connexion with different frameworks. 6 | 7 | Preparing 8 | --------- 9 | 10 | Create a new virtual environment and install the required libraries 11 | with these commands: 12 | 13 | .. code-block:: bash 14 | 15 | $ python -m venv my-venv 16 | $ source my-venv/bin/activate 17 | $ pip install 'connexion[swagger-ui,uvicorn]>=3.1.0' 18 | $ pip install -r requirements.txt 19 | 20 | Running 21 | ------- 22 | 23 | Launch the connexion server with one of these commands: 24 | 25 | .. code-block:: bash 26 | 27 | $ python hello_quart.py 28 | $ python hello_starlette.py 29 | 30 | Now open your browser and view the Swagger UI for these specification files: 31 | 32 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 33 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 34 | -------------------------------------------------------------------------------- /examples/frameworks/hello_quart.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from pathlib import Path 3 | 4 | import connexion 5 | from connexion.decorators import ASGIDecorator 6 | from connexion.resolver import RelativeResolver 7 | from quart import Quart 8 | 9 | app = Quart(__name__) 10 | 11 | 12 | @app.route("/openapi/greeting/", methods=["POST"]) 13 | @app.route("/swagger/greeting/", methods=["POST"]) 14 | @ASGIDecorator() 15 | def post_greeting(name: str, number: t.Optional[int] = None) -> str: 16 | return f"Hello {name}, your number is {number}!" 17 | 18 | 19 | app = connexion.ConnexionMiddleware( 20 | app, 21 | specification_dir="spec/", 22 | resolver=RelativeResolver("hello_quart"), 23 | ) 24 | app.add_api("openapi.yaml", arguments={"title": "Hello World Example"}) 25 | app.add_api("swagger.yaml", arguments={"title": "Hello World Example"}) 26 | 27 | 28 | if __name__ == "__main__": 29 | app.run(f"{Path(__file__).stem}:app", port=8080) 30 | -------------------------------------------------------------------------------- /examples/frameworks/hello_starlette.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from pathlib import Path 3 | 4 | import connexion 5 | from connexion.decorators import StarletteDecorator 6 | from connexion.resolver import RelativeResolver 7 | from starlette.applications import Starlette 8 | from starlette.routing import Route 9 | 10 | 11 | @StarletteDecorator() 12 | def post_greeting(name: str, number: t.Optional[int] = None) -> str: 13 | return f"Hello {name}, your number is {number}!" 14 | 15 | 16 | app = Starlette( 17 | debug=True, 18 | routes=[ 19 | Route("/openapi/greeting/{name}", post_greeting, methods=["POST"]), 20 | Route("/swagger/greeting/{name}", post_greeting, methods=["POST"]), 21 | ], 22 | ) 23 | 24 | app = connexion.ConnexionMiddleware( 25 | app, 26 | specification_dir="spec/", 27 | resolver=RelativeResolver("hello_starlette"), 28 | ) 29 | app.add_api("openapi.yaml", arguments={"title": "Hello World Example"}) 30 | app.add_api("swagger.yaml", arguments={"title": "Hello World Example"}) 31 | 32 | 33 | if __name__ == "__main__": 34 | app.run(f"{Path(__file__).stem}:app", port=8080) 35 | -------------------------------------------------------------------------------- /examples/frameworks/requirements.txt: -------------------------------------------------------------------------------- 1 | quart 2 | starlette -------------------------------------------------------------------------------- /examples/frameworks/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | 3 | info: 4 | title: Hello World 5 | version: "1.0" 6 | servers: 7 | - url: /openapi 8 | 9 | paths: 10 | /greeting/{name}: 11 | post: 12 | summary: Generate greeting 13 | description: Generates a greeting message. 14 | operationId: post_greeting 15 | responses: 16 | '200': 17 | description: greeting response 18 | content: 19 | text/plain: 20 | schema: 21 | type: string 22 | example: "hello dave!" 23 | parameters: 24 | - name: name 25 | in: path 26 | description: Name of the person to greet. 27 | required: true 28 | schema: 29 | type: string 30 | example: "dave" 31 | - name: number 32 | in: query 33 | description: A number. 34 | schema: 35 | type: integer 36 | example: 1 37 | default: 0 38 | 39 | -------------------------------------------------------------------------------- /examples/frameworks/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /swagger 8 | 9 | paths: 10 | /greeting/{name}: 11 | post: 12 | summary: Generate greeting 13 | description: Generates a greeting message. 14 | operationId: post_greeting 15 | produces: 16 | - text/plain; 17 | responses: 18 | '200': 19 | description: greeting response 20 | schema: 21 | type: string 22 | examples: 23 | "text/plain": "Hello John" 24 | parameters: 25 | - name: name 26 | in: path 27 | description: Name of the person to greet. 28 | required: true 29 | type: string 30 | - name: number 31 | in: query 32 | description: A number. 33 | type: integer 34 | default: 0 35 | -------------------------------------------------------------------------------- /examples/helloworld/README.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Hello World Example 3 | =================== 4 | 5 | Preparing 6 | --------- 7 | 8 | Create a new virtual environment and install the required libraries 9 | with these commands: 10 | 11 | .. code-block:: bash 12 | 13 | $ python -m venv my-venv 14 | $ source my-venv/bin/activate 15 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 16 | 17 | Running 18 | ------- 19 | 20 | Launch the connexion server with this command: 21 | 22 | .. code-block:: bash 23 | 24 | $ python hello.py 25 | 26 | Now open your browser and view the Swagger UI for these specification files: 27 | 28 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 29 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 30 | -------------------------------------------------------------------------------- /examples/helloworld/hello.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import connexion 4 | 5 | 6 | def post_greeting(name: str) -> str: 7 | return f"Hello {name}" 8 | 9 | 10 | app = connexion.FlaskApp(__name__, specification_dir="spec/") 11 | app.add_api("openapi.yaml", arguments={"title": "Hello World Example"}) 12 | app.add_api("swagger.yaml", arguments={"title": "Hello World Example"}) 13 | 14 | 15 | if __name__ == "__main__": 16 | app.run(f"{Path(__file__).stem}:app", port=8080) 17 | -------------------------------------------------------------------------------- /examples/helloworld/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | 3 | info: 4 | title: Hello World 5 | version: "1.0" 6 | servers: 7 | - url: /openapi 8 | 9 | paths: 10 | /greeting/{name}: 11 | post: 12 | summary: Generate greeting 13 | description: Generates a greeting message. 14 | operationId: hello.post_greeting 15 | responses: 16 | '200': 17 | description: greeting response 18 | content: 19 | text/plain: 20 | schema: 21 | type: string 22 | example: "hello dave!" 23 | parameters: 24 | - name: name 25 | in: path 26 | description: Name of the person to greet. 27 | required: true 28 | schema: 29 | type: string 30 | example: "dave" 31 | requestBody: 32 | content: 33 | application/json: 34 | schema: 35 | type: object 36 | -------------------------------------------------------------------------------- /examples/helloworld/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /swagger 8 | 9 | paths: 10 | /greeting/{name}: 11 | post: 12 | summary: Generate greeting 13 | description: Generates a greeting message. 14 | operationId: hello.post_greeting 15 | produces: 16 | - text/plain; 17 | responses: 18 | '200': 19 | description: greeting response 20 | schema: 21 | type: string 22 | examples: 23 | "text/plain": "Hello John" 24 | parameters: 25 | - name: name 26 | in: path 27 | description: Name of the person to greet. 28 | required: true 29 | type: string 30 | -------------------------------------------------------------------------------- /examples/helloworld_async/README.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Hello World Example using async App 3 | =================================== 4 | 5 | Preparing 6 | --------- 7 | 8 | Create a new virtual environment and install the required libraries 9 | with these commands: 10 | 11 | .. code-block:: bash 12 | 13 | $ python -m venv my-venv 14 | $ source my-venv/bin/activate 15 | $ pip install 'connexion[swagger-ui,uvicorn]>=3.1.0' 16 | 17 | Running 18 | ------- 19 | 20 | Launch the connexion server with this command: 21 | 22 | .. code-block:: bash 23 | 24 | $ python hello.py 25 | 26 | Now open your browser and view the Swagger UI for these specification files: 27 | 28 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 29 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 30 | -------------------------------------------------------------------------------- /examples/helloworld_async/hello.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import connexion 4 | 5 | 6 | async def test(): 7 | pass 8 | 9 | 10 | async def post_greeting(name: str): 11 | await test() 12 | return f"Hello {name}", 201 13 | 14 | 15 | app = connexion.AsyncApp(__name__, specification_dir="spec") 16 | app.add_api("openapi.yaml", arguments={"title": "Hello World Example"}) 17 | app.add_api("swagger.yaml", arguments={"title": "Hello World Example"}) 18 | 19 | 20 | if __name__ == "__main__": 21 | app.run(f"{Path(__file__).stem}:app", port=8080) 22 | -------------------------------------------------------------------------------- /examples/helloworld_async/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | 3 | info: 4 | title: Hello World 5 | version: "1.0" 6 | servers: 7 | - url: /openapi 8 | 9 | paths: 10 | /greeting/{name}: 11 | post: 12 | summary: Generate greeting 13 | description: Generates a greeting message. 14 | operationId: hello.post_greeting 15 | responses: 16 | '200': 17 | description: greeting response 18 | content: 19 | text/plain: 20 | schema: 21 | type: string 22 | example: "hello dave!" 23 | parameters: 24 | - name: name 25 | in: path 26 | description: Name of the person to greet. 27 | required: true 28 | schema: 29 | type: string 30 | example: "dave" 31 | -------------------------------------------------------------------------------- /examples/helloworld_async/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: Hello World 5 | version: "1.0" 6 | basePath: /swagger 7 | 8 | paths: 9 | /greeting/{name}: 10 | post: 11 | summary: Generate greeting 12 | description: Generates a greeting message. 13 | operationId: hello.post_greeting 14 | responses: 15 | '200': 16 | description: greeting response 17 | schema: 18 | type: string 19 | example: "hello dave!" 20 | parameters: 21 | - name: name 22 | in: path 23 | description: Name of the person to greet. 24 | required: true 25 | type: string 26 | -------------------------------------------------------------------------------- /examples/jwt/README.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | JWT Auth Example 3 | ================ 4 | 5 | .. note:: 6 | 7 | jwt is not supported by swagger 2.0: https://swagger.io/docs/specification/2-0/authentication/ 8 | 9 | Preparing 10 | --------- 11 | 12 | Create a new virtual environment and install the required libraries 13 | with these commands: 14 | 15 | .. code-block:: bash 16 | 17 | $ python -m venv my-venv 18 | $ source my-venv/bin/activate 19 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 20 | $ pip install -r requirements.txt 21 | 22 | Running 23 | ------- 24 | 25 | Launch the connexion server with this command: 26 | 27 | .. code-block:: bash 28 | 29 | $ python app.py 30 | 31 | Now open your browser and view the Swagger UI for the specification file: 32 | 33 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 34 | 35 | Use endpoint **/auth** to generate JWT token, copy it, then click **Authorize** button and paste the token. 36 | Now you can use endpoint **/secret** to check authentication. 37 | -------------------------------------------------------------------------------- /examples/jwt/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic example of a resource server 3 | """ 4 | import time 5 | from pathlib import Path 6 | 7 | import connexion 8 | from jose import JWTError, jwt 9 | from werkzeug.exceptions import Unauthorized 10 | 11 | JWT_ISSUER = "com.zalando.connexion" 12 | JWT_SECRET = "change_this" 13 | JWT_LIFETIME_SECONDS = 600 14 | JWT_ALGORITHM = "HS256" 15 | 16 | 17 | def generate_token(user_id): 18 | timestamp = _current_timestamp() 19 | payload = { 20 | "iss": JWT_ISSUER, 21 | "iat": int(timestamp), 22 | "exp": int(timestamp + JWT_LIFETIME_SECONDS), 23 | "sub": str(user_id), 24 | } 25 | 26 | return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) 27 | 28 | 29 | def decode_token(token): 30 | try: 31 | return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM]) 32 | except JWTError as e: 33 | raise Unauthorized from e 34 | 35 | 36 | def get_secret(user, token_info) -> str: 37 | return """ 38 | You are user_id {user} and the secret is 'wbevuec'. 39 | Decoded token claims: {token_info}. 40 | """.format( 41 | user=user, token_info=token_info 42 | ) 43 | 44 | 45 | def _current_timestamp() -> int: 46 | return int(time.time()) 47 | 48 | 49 | app = connexion.FlaskApp(__name__, specification_dir="spec") 50 | app.add_api("openapi.yaml") 51 | 52 | 53 | if __name__ == "__main__": 54 | app.run(f"{Path(__file__).stem}:app", port=8080) 55 | -------------------------------------------------------------------------------- /examples/jwt/requirements.txt: -------------------------------------------------------------------------------- 1 | python-jose[cryptography] 2 | -------------------------------------------------------------------------------- /examples/jwt/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: JWT Example 4 | version: '1.0' 5 | servers: 6 | - url: /openapi 7 | paths: 8 | /auth/{user_id}: 9 | get: 10 | summary: Return JWT token 11 | operationId: app.generate_token 12 | parameters: 13 | - name: user_id 14 | description: User unique identifier 15 | in: path 16 | required: true 17 | example: 12 18 | schema: 19 | type: integer 20 | responses: 21 | '200': 22 | description: JWT token 23 | content: 24 | 'text/plain': 25 | schema: 26 | type: string 27 | /secret: 28 | get: 29 | summary: Return secret string 30 | operationId: app.get_secret 31 | responses: 32 | '200': 33 | description: secret response 34 | content: 35 | 'text/plain': 36 | schema: 37 | type: string 38 | security: 39 | - jwt: ['secret'] 40 | 41 | components: 42 | securitySchemes: 43 | jwt: 44 | type: http 45 | scheme: bearer 46 | bearerFormat: JWT 47 | x-bearerInfoFunc: app.decode_token 48 | -------------------------------------------------------------------------------- /examples/methodresolver/README.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | MethodViewResolver Example 3 | ========================== 4 | 5 | Preparing 6 | --------- 7 | 8 | Create a new virtual environment and install the required libraries 9 | with these commands: 10 | 11 | .. code-block:: bash 12 | 13 | $ python -m venv my-venv 14 | $ source my-venv/bin/activate 15 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 16 | 17 | Running 18 | ------- 19 | 20 | Launch the connexion server with this command: 21 | 22 | .. code-block:: bash 23 | 24 | $ python app.py 25 | 26 | Now open your browser and view the Swagger UI for the specification file: 27 | 28 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 29 | -------------------------------------------------------------------------------- /examples/methodresolver/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .petsview import PetsView as PetsView 2 | -------------------------------------------------------------------------------- /examples/methodresolver/api/petsview.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from connexion import NoContent 4 | from flask.views import MethodView 5 | 6 | 7 | def example_decorator(f): 8 | """ 9 | the returned view from .as_view can be decorated 10 | the decorator is initialized exactly once per class 11 | """ 12 | 13 | def decorator(*args, **kwargs): 14 | return f(*args, **kwargs) 15 | 16 | return decorator 17 | 18 | 19 | class PetsView(MethodView): 20 | """Create Pets service""" 21 | 22 | decorators = [example_decorator] 23 | pets = {} 24 | 25 | def __init__(self, pets=None): 26 | # the args and kwargs can be provided 27 | # via the MethodViewResolver's class_params dict 28 | if pets is not None: 29 | self.pets = pets 30 | 31 | def post(self, body: dict): 32 | name = body.get("name") 33 | tag = body.get("tag") 34 | count = len(self.pets) 35 | pet = {} 36 | pet["id"] = count + 1 37 | pet["tag"] = tag 38 | pet["name"] = name 39 | pet["last_updated"] = datetime.datetime.now() 40 | self.pets[pet["id"]] = pet 41 | return pet, 201 42 | 43 | def put(self, petId, body: dict): 44 | name = body["name"] 45 | tag = body.get("tag") 46 | pet = self.pets.get(petId, {"id": petId}) 47 | pet["name"] = name 48 | pet["tag"] = tag 49 | pet["last_updated"] = datetime.datetime.now() 50 | self.pets[petId] = pet 51 | return self.pets[petId], 201 52 | 53 | def delete(self, petId): 54 | id_ = int(petId) 55 | if self.pets.get(id_) is None: 56 | return NoContent, 404 57 | del self.pets[id_] 58 | return NoContent, 204 59 | 60 | def get(self, petId=None, limit=100): 61 | if petId is None: 62 | # NOTE: we need to wrap it with list for Python 3 as 63 | # dict_values is not JSON serializable 64 | return list(self.pets.values())[0:limit] 65 | if self.pets.get(petId) is None: 66 | return NoContent, 404 67 | return self.pets[petId] 68 | -------------------------------------------------------------------------------- /examples/methodresolver/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | import connexion 5 | from connexion.resolver import MethodViewResolver 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | 9 | zoo = { 10 | 1: { 11 | "id": 1, 12 | "name": "giraffe", 13 | "tags": ["africa", "yellow", "hoofs", "herbivore", "long neck"], 14 | }, 15 | 2: { 16 | "id": 2, 17 | "name": "lion", 18 | "tags": ["africa", "yellow", "paws", "carnivore", "mane"], 19 | }, 20 | } 21 | 22 | 23 | app = connexion.FlaskApp(__name__, specification_dir="spec/") 24 | 25 | options = {"swagger_ui": True} 26 | app.add_api( 27 | "openapi.yaml", 28 | options=options, 29 | arguments={"title": "MethodViewResolver Example"}, 30 | resolver=MethodViewResolver( 31 | "api", 32 | # class params are entirely optional 33 | # they allow to inject dependencies top down 34 | # so that the app can be wired, in the entrypoint 35 | class_arguments={"PetsView": {"kwargs": {"pets": zoo}}}, 36 | ), 37 | strict_validation=True, 38 | validate_responses=True, 39 | ) 40 | 41 | 42 | if __name__ == "__main__": 43 | app.run(f"{Path(__file__).stem}:app", port=8080) 44 | -------------------------------------------------------------------------------- /examples/oauth2/README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | OAuth2 Example 3 | ============== 4 | 5 | This example demonstrates how to implement a resource server with Connexion. 6 | The app will lookup OAuth2 Bearer tokens with the given token info function. 7 | 8 | Preparing 9 | --------- 10 | 11 | Create a new virtual environment and install the required libraries 12 | with these commands: 13 | 14 | .. code-block:: bash 15 | 16 | $ python -m venv my-venv 17 | $ source my-venv/bin/activate 18 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 19 | 20 | Running 21 | ------- 22 | 23 | Start a mock server in the background, then launch the connexion server, 24 | with these commands: 25 | 26 | .. code-block:: bash 27 | 28 | $ python mock_tokeninfo.py & # start mock in background 29 | $ python app.py 30 | 31 | Now open your browser and view the Swagger UI for these specification files: 32 | 33 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 34 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 35 | 36 | You can use the hardcoded tokens to request the endpoint: 37 | 38 | .. code-block:: bash 39 | 40 | $ curl http://localhost:8080/openapi/secret # missing authentication 41 | $ curl -H 'Authorization: Bearer 123' http://localhost:8080/openapi/secret 42 | $ curl -H 'Authorization: Bearer 456' http://localhost:8080/swagger/secret 43 | 44 | -------------------------------------------------------------------------------- /examples/oauth2/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic example of a resource server 3 | """ 4 | from pathlib import Path 5 | 6 | import connexion 7 | 8 | 9 | def get_secret(user) -> str: 10 | return f"You are: {user}" 11 | 12 | 13 | app = connexion.FlaskApp(__name__, specification_dir="spec") 14 | app.add_api("openapi.yaml") 15 | app.add_api("swagger.yaml") 16 | 17 | 18 | if __name__ == "__main__": 19 | app.run(f"{Path(__file__).stem}:app", port=8080) 20 | -------------------------------------------------------------------------------- /examples/oauth2/mock_tokeninfo.py: -------------------------------------------------------------------------------- 1 | """ 2 | Mock OAuth2 token info 3 | """ 4 | 5 | import connexion 6 | import uvicorn 7 | from connexion import request 8 | 9 | # our hardcoded mock "Bearer" access tokens 10 | TOKENS = {"123": "jdoe", "456": "rms"} 11 | 12 | 13 | def get_tokeninfo() -> dict: 14 | try: 15 | _, access_token = request.headers["Authorization"].split() 16 | except Exception: 17 | access_token = "" 18 | 19 | sub = TOKENS.get(access_token) 20 | 21 | if not sub: 22 | return "No such token", 401 23 | 24 | return {"sub": sub, "scope": ["uid"]} 25 | 26 | 27 | if __name__ == "__main__": 28 | app = connexion.FlaskApp(__name__, specification_dir="spec") 29 | app.add_api("mock_tokeninfo.yaml") 30 | uvicorn.run(app, port=7979) 31 | -------------------------------------------------------------------------------- /examples/oauth2/spec/mock_tokeninfo.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: Mock OAuth Token Info 5 | version: "1.0" 6 | 7 | paths: 8 | /tokeninfo: 9 | get: 10 | summary: OAuth2 token info 11 | operationId: mock_tokeninfo.get_tokeninfo 12 | responses: 13 | '200': 14 | description: Token info object 15 | schema: 16 | type: object 17 | properties: 18 | uid: 19 | type: string 20 | scope: 21 | type: array 22 | items: 23 | type: string 24 | -------------------------------------------------------------------------------- /examples/oauth2/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | 3 | info: 4 | title: OAuth Example 5 | version: "1.0" 6 | 7 | servers: 8 | - url: /openapi 9 | 10 | paths: 11 | /secret: 12 | get: 13 | summary: Return secret string 14 | operationId: app.get_secret 15 | responses: 16 | '200': 17 | description: secret response 18 | content: 19 | 'text/plain': 20 | schema: 21 | type: string 22 | security: 23 | # enable authentication and require the "uid" scope for this endpoint 24 | - oauth2: ['uid'] 25 | 26 | components: 27 | securitySchemes: 28 | oauth2: 29 | type: oauth2 30 | x-tokenInfoUrl: http://localhost:7979/tokeninfo 31 | flows: 32 | implicit: 33 | authorizationUrl: https://example.com/oauth2/dialog 34 | # the token info URL is hardcoded for our mock_tokeninfo.py script 35 | # you can also pass it as an environment variable TOKENINFO_URL 36 | scopes: 37 | uid: Unique identifier of the user accessing the service. 38 | -------------------------------------------------------------------------------- /examples/oauth2/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: OAuth Example 5 | version: "1.0" 6 | 7 | basePath: /swagger 8 | 9 | paths: 10 | /secret: 11 | get: 12 | summary: Return secret string 13 | operationId: app.get_secret 14 | responses: 15 | '200': 16 | description: secret response 17 | schema: 18 | type: string 19 | security: 20 | # enable authentication and require the "uid" scope for this endpoint 21 | - oauth2: ['uid'] 22 | 23 | securityDefinitions: 24 | oauth2: 25 | type: oauth2 26 | flow: implicit 27 | authorizationUrl: https://example.com/oauth2/dialog 28 | # the token info URL is hardcoded for our mock_tokeninfo.py script 29 | # you can also pass it as an environment variable TOKENINFO_URL 30 | x-tokenInfoUrl: http://localhost:7979/tokeninfo 31 | scopes: 32 | uid: Unique identifier of the user accessing the service. 33 | -------------------------------------------------------------------------------- /examples/oauth2_local_tokeninfo/README.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | OAuth2 Local Validation Example 3 | =============================== 4 | 5 | This example demonstrates how to implement a resource server with Connexion. 6 | The app will lookup OAuth2 Bearer tokens in a static map. 7 | 8 | Preparing 9 | --------- 10 | 11 | Create a new virtual environment and install the required libraries 12 | with these commands: 13 | 14 | .. code-block:: bash 15 | 16 | $ python -m venv my-venv 17 | $ source my-venv/bin/activate 18 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 19 | 20 | Running 21 | ------- 22 | 23 | .. code-block:: bash 24 | 25 | $ python app.py 26 | 27 | Now open your browser and view the Swagger UI for these specification files: 28 | 29 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 30 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 31 | 32 | You can use the hardcoded tokens to request the endpoint: 33 | 34 | .. code-block:: bash 35 | 36 | $ curl http://localhost:8080/openapi/secret # missing authentication 37 | $ curl -H 'Authorization: Bearer 123' http://localhost:8080/openapi/secret 38 | $ curl -H 'Authorization: Bearer 456' http://localhost:8080/swagger/secret 39 | -------------------------------------------------------------------------------- /examples/oauth2_local_tokeninfo/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic example of a resource server 3 | """ 4 | from pathlib import Path 5 | 6 | import connexion 7 | 8 | # our hardcoded mock "Bearer" access tokens 9 | TOKENS = {"123": "jdoe", "456": "rms"} 10 | 11 | 12 | def get_secret(user) -> str: 13 | return f"You are: {user}" 14 | 15 | 16 | def token_info(token) -> dict: 17 | sub = TOKENS.get(token) 18 | if not sub: 19 | return None 20 | return {"sub": sub, "scope": ["uid"]} 21 | 22 | 23 | app = connexion.FlaskApp(__name__, specification_dir="spec") 24 | app.add_api("openapi.yaml") 25 | app.add_api("swagger.yaml") 26 | 27 | 28 | if __name__ == "__main__": 29 | app.run(f"{Path(__file__).stem}:app", port=8080) 30 | -------------------------------------------------------------------------------- /examples/oauth2_local_tokeninfo/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | 3 | info: 4 | title: OAuth Example 5 | version: "1.0" 6 | 7 | servers: 8 | - url: /openapi 9 | 10 | paths: 11 | /secret: 12 | get: 13 | summary: Return secret string 14 | operationId: app.get_secret 15 | responses: 16 | '200': 17 | description: secret response 18 | content: 19 | text/plain: 20 | schema: 21 | type: string 22 | security: 23 | # enable authentication and require the "uid" scope for this endpoint 24 | - oauth2: ['uid'] 25 | 26 | components: 27 | securitySchemes: 28 | oauth2: 29 | type: oauth2 30 | x-tokenInfoFunc: app.token_info 31 | flows: 32 | implicit: 33 | authorizationUrl: https://example.com/oauth2/dialog 34 | scopes: 35 | uid: Unique identifier of the user accessing the service. 36 | -------------------------------------------------------------------------------- /examples/oauth2_local_tokeninfo/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: OAuth Example 5 | version: "1.0" 6 | 7 | basePath: /swagger 8 | 9 | paths: 10 | /secret: 11 | get: 12 | summary: Return secret string 13 | operationId: app.get_secret 14 | responses: 15 | '200': 16 | description: secret response 17 | schema: 18 | type: string 19 | security: 20 | # enable authentication and require the "uid" scope for this endpoint 21 | - oauth2: ['uid'] 22 | 23 | securityDefinitions: 24 | oauth2: 25 | type: oauth2 26 | flow: implicit 27 | authorizationUrl: https://example.com/oauth2/dialog 28 | x-tokenInfoFunc: app.token_info 29 | scopes: 30 | uid: Unique identifier of the user accessing the service. 31 | -------------------------------------------------------------------------------- /examples/restyresolver/README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | RestyResolver Example 3 | ===================== 4 | 5 | Preparing 6 | --------- 7 | 8 | Create a new virtual environment and install the required libraries 9 | with these commands: 10 | 11 | .. code-block:: bash 12 | 13 | $ python -m venv my-venv 14 | $ source my-venv/bin/activate 15 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 16 | 17 | Running 18 | ------- 19 | 20 | Launch the connexion server with this command: 21 | 22 | .. code-block:: bash 23 | 24 | $ python resty.py 25 | 26 | Now open your browser and view the Swagger UI for these specification files: 27 | 28 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 29 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 30 | -------------------------------------------------------------------------------- /examples/restyresolver/api/__init__.py: -------------------------------------------------------------------------------- 1 | import api.pets # noqa 2 | -------------------------------------------------------------------------------- /examples/restyresolver/api/pets.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from connexion import NoContent 4 | 5 | pets = {} 6 | 7 | 8 | def post(body): 9 | name = body.get("name") 10 | tag = body.get("tag") 11 | count = len(pets) 12 | pet = {} 13 | pet["id"] = count + 1 14 | pet["tag"] = tag 15 | pet["name"] = name 16 | pet["last_updated"] = datetime.datetime.now() 17 | pets[pet["id"]] = pet 18 | return pet, 201 19 | 20 | 21 | def put(body): 22 | id_ = body["id"] 23 | name = body["name"] 24 | tag = body.get("tag") 25 | id_ = int(id_) 26 | pet = pets.get(id_, {"id": id_}) 27 | pet["name"] = name 28 | pet["tag"] = tag 29 | pet["last_updated"] = datetime.datetime.now() 30 | pets[id_] = pet 31 | return pets[id_] 32 | 33 | 34 | def delete(id_): 35 | id_ = int(id_) 36 | if pets.get(id_) is None: 37 | return NoContent, 404 38 | del pets[id_] 39 | return NoContent, 204 40 | 41 | 42 | def get(petId): 43 | id_ = int(petId) 44 | if pets.get(id_) is None: 45 | return NoContent, 404 46 | return pets[id_] 47 | 48 | 49 | def search(limit=100): 50 | # NOTE: we need to wrap it with list for Python 3 as dict_values is not JSON serializable 51 | return list(pets.values())[0:limit] 52 | -------------------------------------------------------------------------------- /examples/restyresolver/resty.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | import connexion 5 | from connexion.resolver import RestyResolver 6 | 7 | logging.basicConfig(level=logging.INFO) 8 | 9 | app = connexion.FlaskApp(__name__, specification_dir="spec") 10 | app.add_api( 11 | "openapi.yaml", 12 | arguments={"title": "RestyResolver Example"}, 13 | resolver=RestyResolver("api"), 14 | ) 15 | app.add_api( 16 | "swagger.yaml", 17 | arguments={"title": "RestyResolver Example"}, 18 | resolver=RestyResolver("api"), 19 | ) 20 | 21 | 22 | if __name__ == "__main__": 23 | app.run(f"{Path(__file__).stem}:app", port=8080) 24 | -------------------------------------------------------------------------------- /examples/restyresolver/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | version: 1.0.0 4 | title: Swagger Petstore 5 | license: 6 | name: MIT 7 | servers: 8 | - url: /openapi 9 | paths: 10 | /pets: 11 | get: 12 | summary: List all pets 13 | tags: 14 | - pets 15 | parameters: 16 | - name: limit 17 | in: query 18 | description: How many items to return at one time (max 100) 19 | required: false 20 | schema: 21 | type: integer 22 | format: int32 23 | responses: 24 | '200': 25 | description: An paged array of pets 26 | headers: 27 | x-next: 28 | description: A link to the next page of responses 29 | schema: 30 | type: string 31 | content: 32 | application/json: 33 | schema: 34 | $ref: "#/components/schemas/Pets" 35 | default: 36 | description: unexpected error 37 | content: 38 | application/json: 39 | schema: 40 | $ref: "#/components/schemas/Error" 41 | post: 42 | summary: Create a pet 43 | tags: 44 | - pets 45 | requestBody: 46 | description: Pet to add to the system 47 | content: 48 | application/json: 49 | schema: 50 | $ref: "#/components/schemas/Pet" 51 | responses: 52 | '201': 53 | description: Null response 54 | default: 55 | description: unexpected error 56 | content: 57 | application/json: 58 | schema: 59 | $ref: "#/components/schemas/Error" 60 | put: 61 | summary: Update a pet 62 | tags: 63 | - pets 64 | requestBody: 65 | description: Pet to add to the system 66 | content: 67 | application/json: 68 | schema: 69 | allOf: 70 | - $ref: "#/components/schemas/Pet" 71 | - type: object 72 | required: 73 | - id 74 | properties: 75 | id: 76 | type: integer 77 | format: int64 78 | example: 79 | id: 1 80 | name: chester 81 | tag: sleepy 82 | responses: 83 | '201': 84 | description: Null response 85 | default: 86 | description: unexpected error 87 | content: 88 | application/json: 89 | schema: 90 | $ref: "#/components/schemas/Error" 91 | 92 | 93 | /pets/{petId}: 94 | get: 95 | summary: Info for a specific pet 96 | tags: 97 | - pets 98 | parameters: 99 | - name: petId 100 | in: path 101 | required: true 102 | description: The id of the pet to retrieve 103 | schema: 104 | type: string 105 | responses: 106 | '200': 107 | description: Expected response to a valid request 108 | content: 109 | application/json: 110 | schema: 111 | $ref: "#/components/schemas/Pets" 112 | default: 113 | description: unexpected error 114 | content: 115 | application/json: 116 | schema: 117 | $ref: "#/components/schemas/Error" 118 | components: 119 | schemas: 120 | Pet: 121 | required: 122 | - name 123 | properties: 124 | id: 125 | readOnly: true 126 | type: integer 127 | format: int64 128 | name: 129 | type: string 130 | tag: 131 | type: string 132 | example: 133 | name: chester 134 | tag: fluffy 135 | Pets: 136 | type: array 137 | items: 138 | $ref: "#/components/schemas/Pet" 139 | Error: 140 | required: 141 | - code 142 | - message 143 | properties: 144 | code: 145 | type: integer 146 | format: int32 147 | message: 148 | type: string 149 | -------------------------------------------------------------------------------- /examples/restyresolver/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /swagger 8 | 9 | paths: 10 | /pets: 11 | get: 12 | responses: 13 | '200': 14 | description: 'Fetch a list of pets' 15 | schema: 16 | type: array 17 | items: 18 | $ref: '#/definitions/Pet' 19 | post: 20 | parameters: 21 | - in: body 22 | name: pet 23 | required: true 24 | schema: 25 | $ref: '#/definitions/PetRegistration' 26 | responses: 27 | '201': 28 | description: 'Register a new pet' 29 | 30 | '/pets/{id}': 31 | put: 32 | parameters: 33 | - in: path 34 | name: id 35 | required: true 36 | type: integer 37 | - in: body 38 | name: pet 39 | required: true 40 | schema: 41 | $ref: '#/definitions/Pet' 42 | responses: 43 | '200': 44 | description: 'Update a pet by ID' 45 | delete: 46 | parameters: 47 | - in: path 48 | name: id 49 | required: true 50 | type: integer 51 | responses: 52 | '204': 53 | description: 'Delete a pet by ID' 54 | get: 55 | parameters: 56 | - in: path 57 | name: id 58 | required: true 59 | type: integer 60 | responses: 61 | '200': 62 | description: 'Fetch a pet by ID' 63 | schema: 64 | $ref: '#/definitions/Pet' 65 | 66 | definitions: 67 | PetRegistration: 68 | type: object 69 | properties: 70 | name: { type: string } 71 | Pet: 72 | type: object 73 | properties: 74 | id: 75 | type: integer 76 | format: int64 77 | name: { type: string } 78 | registered: 79 | type: string 80 | format: date-time -------------------------------------------------------------------------------- /examples/reverseproxy/README.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Reverse Proxy Example 3 | ===================== 4 | 5 | This example demonstrates how to run a connexion application behind a path-altering reverse proxy. 6 | 7 | You can set the path in three ways: 8 | 9 | - Via the Middleware 10 | .. code-block:: 11 | 12 | app = ReverseProxied(app, root_path="/reverse_proxied/") 13 | 14 | - Via the ASGI server 15 | .. code-block:: 16 | 17 | uvicorn ... --root_path="/reverse_proxied/" 18 | 19 | - By using the ``X-Forwarded-Path`` header in your proxy server. Eg in nginx: 20 | .. code-block:: 21 | 22 | location /proxied { 23 | proxy_pass http://192.168.0.1:5001; 24 | proxy_set_header Host $host; 25 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 26 | proxy_set_header X-Forwarded-Proto $scheme; 27 | proxy_set_header X-Forwarded-Path /proxied; 28 | } 29 | 30 | To run this example, install Connexion from PyPI: 31 | .. code-block:: 32 | 33 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 34 | 35 | and then run it either directly 36 | .. code-block:: 37 | 38 | $ python app.py 39 | 40 | or using uvicorn (or another async server): 41 | .. code-block:: 42 | $ uvicorn --factory app:create_app --port 8080 43 | 44 | If your proxy server is running at http://localhost:8080/revers_proxied/, you can go to 45 | http://localhost:8080/reverse_proxied/openapi/ui/ to see the Swagger UI. 46 | 47 | Or you can test this using the ``X-Forwarded-Path`` header to modify the reverse proxy path. 48 | For example, note the servers block: 49 | 50 | .. code-block:: bash 51 | 52 | curl -H "X-Forwarded-Path: /banana/" http://localhost:8080/openapi/openapi.json 53 | 54 | { 55 | "servers" : [ 56 | { 57 | "url" : "/banana/openapi" 58 | } 59 | ], 60 | "paths" : { 61 | "/hello" : { 62 | "get" : { 63 | "responses" : { 64 | "200" : { 65 | "description" : "hello", 66 | "content" : { 67 | "text/plain" : { 68 | "schema" : { 69 | "type" : "string" 70 | } 71 | } 72 | } 73 | } 74 | }, 75 | "operationId" : "app.hello", 76 | "summary" : "say hi" 77 | } 78 | } 79 | }, 80 | "openapi" : "3.0.0", 81 | "info" : { 82 | "version" : "1.0", 83 | "title" : "Path-Altering Reverse Proxy Example" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/reverseproxy/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | example of connexion running behind a path-altering reverse-proxy 3 | 4 | NOTE this demo is not secure by default!! 5 | You'll want to make sure these headers are coming from your proxy, and not 6 | directly from users on the web! 7 | 8 | """ 9 | import logging 10 | from pathlib import Path 11 | 12 | import connexion 13 | import uvicorn 14 | from starlette.types import Receive, Scope, Send 15 | 16 | 17 | class ReverseProxied: 18 | """Wrap the application in this middleware and configure the 19 | reverse proxy to add these headers, to let you quietly bind 20 | this to a URL other than / and to an HTTP scheme that is 21 | different than what is used locally. 22 | 23 | In nginx: 24 | 25 | location /proxied { 26 | proxy_pass http://192.168.0.1:5001; 27 | proxy_set_header Host $host; 28 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 29 | proxy_set_header X-Forwarded-Proto $scheme; 30 | proxy_set_header X-Forwarded-Path /proxied; 31 | } 32 | 33 | :param app: the WSGI application 34 | :param root_path: override the default script name (path) 35 | :param scheme: override the default scheme 36 | :param server: override the default server 37 | """ 38 | 39 | def __init__(self, app, root_path=None, scheme=None, server=None): 40 | self.app = app 41 | self.root_path = root_path 42 | self.scheme = scheme 43 | self.server = server 44 | 45 | async def __call__(self, scope: Scope, receive: Receive, send: Send): 46 | logging.warning( 47 | "this demo is not secure by default!! " 48 | "You'll want to make sure these headers are coming from your proxy, " 49 | "and not directly from users on the web!" 50 | ) 51 | root_path = scope.get("root_path") or self.root_path 52 | for header, value in scope.get("headers", []): 53 | if header == b"x-forwarded-path": 54 | root_path = value.decode() 55 | break 56 | if root_path: 57 | root_path = "/" + root_path.strip("/") 58 | scope["root_path"] = root_path 59 | scope["path"] = root_path + scope.get("path", "") 60 | scope["raw_path"] = root_path.encode() + scope.get("raw_path", "") 61 | 62 | scope["scheme"] = scope.get("scheme") or self.scheme 63 | scope["server"] = scope.get("server") or (self.server, None) 64 | 65 | return await self.app(scope, receive, send) 66 | 67 | 68 | def hello(): 69 | return "hello" 70 | 71 | 72 | def create_app(): 73 | app = connexion.FlaskApp(__name__, specification_dir="spec") 74 | app.add_api("openapi.yaml") 75 | app.add_api("swagger.yaml") 76 | app.middleware = ReverseProxied(app.middleware, root_path="/reverse_proxied/") 77 | return app 78 | 79 | 80 | if __name__ == "__main__": 81 | uvicorn.run( 82 | f"{Path(__file__).stem}:create_app", factory=True, port=8080, proxy_headers=True 83 | ) 84 | -------------------------------------------------------------------------------- /examples/reverseproxy/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Path-Altering Reverse Proxy Example 4 | version: '1.0' 5 | servers: 6 | - url: /openapi 7 | paths: 8 | /hello: 9 | get: 10 | summary: say hi 11 | operationId: app.hello 12 | responses: 13 | '200': 14 | description: hello 15 | content: 16 | text/plain: 17 | schema: 18 | type: string 19 | -------------------------------------------------------------------------------- /examples/reverseproxy/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: Path-Altering Reverse Proxy Example 4 | version: '1.0' 5 | basePath: /swagger 6 | paths: 7 | /hello: 8 | get: 9 | summary: say hi 10 | operationId: app.hello 11 | responses: 12 | '200': 13 | description: hello 14 | schema: 15 | type: string 16 | -------------------------------------------------------------------------------- /examples/splitspecs/README.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Split Specs Example 3 | =================== 4 | 5 | This example demonstrates split specifications and relative references. 6 | The OpenAPI specification and the Swagger specification are stored in 7 | multiple files and use references that are resolved by Connexion. 8 | 9 | Preparing 10 | --------- 11 | 12 | Create a new virtual environment and install the required libraries 13 | with these commands: 14 | 15 | .. code-block:: bash 16 | $ python -m venv my-venv 17 | $ source my-venv/bin/activate 18 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 19 | 20 | Running 21 | ------- 22 | 23 | Launch the connexion server with this command: 24 | 25 | Running: 26 | 27 | .. code-block:: bash 28 | 29 | $ python app.py 30 | 31 | Now open your browser and view the Swagger UI for these specification files: 32 | 33 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 34 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 35 | -------------------------------------------------------------------------------- /examples/splitspecs/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import connexion 4 | 5 | pets = { 6 | 1: {"name": "Aldo", "registered": "2022-11-28T00:00:00Z"}, 7 | 2: {"name": "Bailey", "registered": "2023-11-28T11:11:11Z"}, 8 | 3: {"name": "Hugo", "registered": "2024-11-28T22:22:22Z"}, 9 | } 10 | 11 | 12 | def get(petId): 13 | id_ = int(petId) 14 | if pets.get(id_) is None: 15 | return connexion.NoContent, 404 16 | return pets[id_] 17 | 18 | 19 | def show(): 20 | # NOTE: we need to wrap it with list for Python 3 as dict_values is not JSON serializable 21 | return list(pets.values()) 22 | 23 | 24 | app = connexion.FlaskApp(__name__, specification_dir="spec/") 25 | app.add_api("openapi.yaml", arguments={"title": "Pet Store Rel Ref Example"}) 26 | app.add_api("swagger.yaml", arguments={"title": "Pet Store Rel Ref Example"}) 27 | 28 | 29 | if __name__ == "__main__": 30 | app.run(f"{Path(__file__).stem}:app", port=8080) 31 | -------------------------------------------------------------------------------- /examples/splitspecs/spec/components.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | Pet: 4 | required: 5 | - name 6 | properties: 7 | id: 8 | type: integer 9 | format: int64 10 | readOnly: true 11 | example: 1 12 | name: 13 | type: string 14 | example: fluffy 15 | registered: 16 | type: string 17 | readOnly: true 18 | example: 2019-01-16T23:52:54Z 19 | 20 | Pets: 21 | type: array 22 | items: 23 | $ref: "#/components/schemas/Pet" 24 | 25 | Error: 26 | properties: 27 | code: 28 | type: integer 29 | format: int32 30 | message: 31 | type: string 32 | required: 33 | - code 34 | - message 35 | -------------------------------------------------------------------------------- /examples/splitspecs/spec/definitions.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | Pet: 3 | type: object 4 | required: 5 | - name 6 | properties: 7 | id: 8 | type: integer 9 | format: int64 10 | example: 1 11 | name: 12 | type: string 13 | example: fluffy 14 | registered: 15 | type: string 16 | format: date-time 17 | example: 2019-01-16T23:52:54Z 18 | 19 | Pets: 20 | type: array 21 | items: 22 | $ref: "#/definitions/Pet" 23 | 24 | Error: 25 | type: object 26 | properties: 27 | code: 28 | type: integer 29 | format: int32 30 | message: 31 | type: string 32 | required: 33 | - code 34 | - message 35 | -------------------------------------------------------------------------------- /examples/splitspecs/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | 3 | info: 4 | version: 1.0.0 5 | title: Swagger Petstore 6 | license: 7 | name: MIT 8 | 9 | servers: 10 | - url: /openapi 11 | 12 | paths: 13 | /pets: 14 | get: 15 | summary: List all pets 16 | operationId: app.show 17 | responses: 18 | '200': 19 | description: A list of pets 20 | content: 21 | application/json: 22 | schema: 23 | $ref: "components.yaml#/components/schemas/Pets" 24 | default: 25 | description: Unexpected error 26 | content: 27 | application/json: 28 | schema: 29 | $ref: "components.yaml#/components/schemas/Error" 30 | 31 | '/pets/{petId}': 32 | get: 33 | summary: Info for a specific pet 34 | operationId: app.get 35 | parameters: 36 | - name: petId 37 | in: path 38 | description: Id of the pet to get. 39 | required: true 40 | schema: 41 | type: integer 42 | example: 1 43 | responses: 44 | '200': 45 | description: Expected response to a valid request 46 | content: 47 | application/json: 48 | schema: 49 | $ref: "components.yaml#/components/schemas/Pet" 50 | default: 51 | description: Unexpected error 52 | content: 53 | application/json: 54 | schema: 55 | $ref: "components.yaml#/components/schemas/Error" 56 | -------------------------------------------------------------------------------- /examples/splitspecs/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /swagger 8 | 9 | paths: 10 | /pets: 11 | get: 12 | summary: List all pets 13 | operationId: app.show 14 | responses: 15 | '200': 16 | description: List all pets 17 | schema: 18 | type: array 19 | items: 20 | $ref: 'definitions.yaml#/definitions/Pets' 21 | default: 22 | description: Unexpected Error 23 | schema: 24 | $ref: 'definitions.yaml#/definitions/Error' 25 | 26 | '/pets/{petId}': 27 | get: 28 | summary: Info for a specific pet 29 | operationId: app.get 30 | parameters: 31 | - name: petId 32 | in: path 33 | required: true 34 | type: integer 35 | minimum: 1 36 | description: Parameter description in Markdown. 37 | responses: 38 | '200': 39 | description: Expected response to a valid request 40 | schema: 41 | $ref: 'definitions.yaml#/definitions/Pet' 42 | default: 43 | description: Unexpected Error 44 | schema: 45 | $ref: 'definitions.yaml#/definitions/Error' 46 | -------------------------------------------------------------------------------- /examples/sqlalchemy/README.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | SQLAlchemy Example 3 | ================== 4 | 5 | .. note:: 6 | 7 | A simple example of how one might use SQLAlchemy as a backing store for a 8 | Connexion based application. 9 | 10 | Preparing 11 | --------- 12 | 13 | Create a new virtual environment and install the required libraries 14 | with these commands: 15 | 16 | .. code-block:: bash 17 | 18 | $ python -m venv my-venv 19 | $ source my-venv/bin/activate 20 | $ pip install 'connexion[flask,swagger-ui,uvicorn]>=3.1.0' 21 | $ pip install -r requirements.txt 22 | 23 | Running 24 | ------- 25 | 26 | Launch the connexion server with this command: 27 | 28 | .. code-block:: bash 29 | 30 | $ python app.py 31 | 32 | Now open your browser and view the Swagger UI for these specification files: 33 | 34 | * http://localhost:8080/openapi/ui/ for the OpenAPI 3 spec 35 | * http://localhost:8080/swagger/ui/ for the Swagger 2 spec 36 | -------------------------------------------------------------------------------- /examples/sqlalchemy/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime, timezone 3 | 4 | import connexion 5 | import orm 6 | from connexion import NoContent 7 | 8 | 9 | def get_pets(limit, animal_type=None): 10 | with db_session_factory() as db_session: 11 | q = db_session.query(orm.Pet) 12 | if animal_type: 13 | q = q.filter(orm.Pet.animal_type == animal_type) 14 | return [p.dump() for p in q][:limit] 15 | 16 | 17 | def get_pet(pet_id): 18 | with db_session_factory() as db_session: 19 | pet = db_session.query(orm.Pet).filter(orm.Pet.id == pet_id).one_or_none() 20 | return pet.dump() if pet is not None else ("Not found", 404) 21 | 22 | 23 | def put_pet(pet_id, pet): 24 | with db_session_factory() as db_session: 25 | p = db_session.query(orm.Pet).filter(orm.Pet.id == pet_id).one_or_none() 26 | pet["id"] = pet_id 27 | if p is not None: 28 | logging.info("Updating pet %s..", pet_id) 29 | p.update(**pet) 30 | else: 31 | logging.info("Creating pet %s..", pet_id) 32 | pet["created"] = datetime.now(timezone.utc) 33 | db_session.add(orm.Pet(**pet)) 34 | db_session.commit() 35 | return NoContent, (200 if p is not None else 201) 36 | 37 | 38 | def delete_pet(pet_id): 39 | with db_session_factory() as db_session: 40 | pet = db_session.query(orm.Pet).filter(orm.Pet.id == pet_id).one_or_none() 41 | if pet is not None: 42 | logging.info("Deleting pet %s..", pet_id) 43 | db_session.delete(pet) 44 | db_session.commit() 45 | return NoContent, 204 46 | else: 47 | return NoContent, 404 48 | 49 | 50 | logging.basicConfig(level=logging.INFO) 51 | db_session_factory = orm.init_db() 52 | pets = { 53 | 1: {"name": "Aldo", "animal_type": "cat"}, 54 | 2: {"name": "Bailey", "animal_type": "dog"}, 55 | 3: {"name": "Hugo", "animal_type": "cat"}, 56 | } 57 | for id_, pet in pets.items(): 58 | put_pet(id_, pet) 59 | app = connexion.FlaskApp(__name__, specification_dir="spec") 60 | app.add_api("openapi.yaml") 61 | app.add_api("swagger.yaml") 62 | 63 | 64 | if __name__ == "__main__": 65 | app.run(port=8080, reload=False) 66 | -------------------------------------------------------------------------------- /examples/sqlalchemy/orm.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, DateTime, String, create_engine 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import sessionmaker 4 | from sqlalchemy.pool import StaticPool 5 | 6 | Base = declarative_base() 7 | 8 | 9 | class Pet(Base): 10 | __tablename__ = "pets" 11 | id = Column(String(20), primary_key=True) 12 | name = Column(String(100)) 13 | animal_type = Column(String(20)) 14 | created = Column(DateTime()) 15 | 16 | def update(self, id=None, name=None, animal_type=None, tags=None, created=None): 17 | if name is not None: 18 | self.name = name 19 | if animal_type is not None: 20 | self.animal_type = animal_type 21 | if created is not None: 22 | self.created = created 23 | 24 | def dump(self): 25 | return {k: v for k, v in vars(self).items() if not k.startswith("_")} 26 | 27 | 28 | def init_db(): 29 | """ 30 | Initialize the database and return a sessionmaker object. 31 | `check_same_thread` and `StaticPool` are helpful for unit testing of 32 | in-memory sqlite databases; they should not be used in production. 33 | https://stackoverflow.com/questions/6519546/scoped-sessionsessionmaker-or-plain-sessionmaker-in-sqlalchemy 34 | """ 35 | engine = create_engine( 36 | url="sqlite:///:memory:", 37 | connect_args={"check_same_thread": False}, 38 | poolclass=StaticPool, 39 | ) 40 | Base.metadata.create_all(bind=engine) 41 | return sessionmaker(autocommit=False, autoflush=False, bind=engine) 42 | -------------------------------------------------------------------------------- /examples/sqlalchemy/requirements.txt: -------------------------------------------------------------------------------- 1 | SQLAlchemy>=1.0.13 2 | -------------------------------------------------------------------------------- /examples/sqlalchemy/spec/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | servers: 3 | - url: /openapi 4 | info: 5 | title: Pet Shop Example API 6 | version: '0.1' 7 | paths: 8 | /pets: 9 | get: 10 | tags: 11 | - Pets 12 | operationId: app.get_pets 13 | summary: Get all pets 14 | parameters: 15 | - name: animal_type 16 | in: query 17 | schema: 18 | type: string 19 | pattern: '^[a-zA-Z0-9]*$' 20 | - name: limit 21 | in: query 22 | schema: 23 | type: integer 24 | minimum: 0 25 | default: 100 26 | responses: 27 | '200': 28 | description: Return pets 29 | content: 30 | application/json: 31 | schema: 32 | type: array 33 | items: 34 | $ref: '#/components/schemas/Pet' 35 | '/pets/{pet_id}': 36 | get: 37 | tags: 38 | - Pets 39 | operationId: app.get_pet 40 | summary: Get a single pet 41 | parameters: 42 | - $ref: '#/components/parameters/pet_id' 43 | responses: 44 | '200': 45 | description: Return pet 46 | content: 47 | application/json: 48 | schema: 49 | $ref: '#/components/schemas/Pet' 50 | '404': 51 | description: Pet does not exist 52 | put: 53 | tags: 54 | - Pets 55 | operationId: app.put_pet 56 | summary: Create or update a pet 57 | parameters: 58 | - $ref: '#/components/parameters/pet_id' 59 | responses: 60 | '200': 61 | description: Pet updated 62 | '201': 63 | description: New pet created 64 | requestBody: 65 | x-body-name: pet 66 | content: 67 | application/json: 68 | schema: 69 | $ref: '#/components/schemas/Pet' 70 | delete: 71 | tags: 72 | - Pets 73 | operationId: app.delete_pet 74 | summary: Remove a pet 75 | parameters: 76 | - $ref: '#/components/parameters/pet_id' 77 | responses: 78 | '204': 79 | description: Pet was deleted 80 | '404': 81 | description: Pet does not exist 82 | components: 83 | parameters: 84 | pet_id: 85 | name: pet_id 86 | description: Pet's Unique identifier 87 | in: path 88 | required: true 89 | schema: 90 | type: string 91 | pattern: '^[a-zA-Z0-9-]+$' 92 | schemas: 93 | Pet: 94 | type: object 95 | required: 96 | - name 97 | - animal_type 98 | properties: 99 | id: 100 | type: string 101 | description: Unique identifier 102 | example: '123' 103 | readOnly: true 104 | name: 105 | type: string 106 | description: Pet's name 107 | example: Susie 108 | minLength: 1 109 | maxLength: 100 110 | animal_type: 111 | type: string 112 | description: Kind of animal 113 | example: cat 114 | minLength: 1 115 | created: 116 | type: string 117 | format: date-time 118 | description: Creation time 119 | example: '2015-07-07T15:49:51.230+02:00' 120 | readOnly: true 121 | -------------------------------------------------------------------------------- /examples/sqlalchemy/spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | info: 3 | title: Pet Shop Example API 4 | version: "0.1" 5 | basePath: /swagger 6 | consumes: 7 | - application/json 8 | produces: 9 | - application/json 10 | paths: 11 | /pets: 12 | get: 13 | tags: [Pets] 14 | operationId: app.get_pets 15 | summary: Get all pets 16 | parameters: 17 | - name: animal_type 18 | in: query 19 | type: string 20 | pattern: "^[a-zA-Z0-9]*$" 21 | - name: limit 22 | in: query 23 | type: integer 24 | minimum: 0 25 | default: 100 26 | responses: 27 | '200': 28 | description: Return pets 29 | schema: 30 | type: array 31 | items: 32 | $ref: '#/definitions/Pet' 33 | /pets/{pet_id}: 34 | get: 35 | tags: [Pets] 36 | operationId: app.get_pet 37 | summary: Get a single pet 38 | parameters: 39 | - $ref: '#/parameters/pet_id' 40 | responses: 41 | '200': 42 | description: Return pet 43 | schema: 44 | $ref: '#/definitions/Pet' 45 | '404': 46 | description: Pet does not exist 47 | put: 48 | tags: [Pets] 49 | operationId: app.put_pet 50 | summary: Create or update a pet 51 | parameters: 52 | - $ref: '#/parameters/pet_id' 53 | - name: pet 54 | in: body 55 | schema: 56 | $ref: '#/definitions/Pet' 57 | responses: 58 | '200': 59 | description: Pet updated 60 | '201': 61 | description: New pet created 62 | delete: 63 | tags: [Pets] 64 | operationId: app.delete_pet 65 | summary: Remove a pet 66 | parameters: 67 | - $ref: '#/parameters/pet_id' 68 | responses: 69 | '204': 70 | description: Pet was deleted 71 | '404': 72 | description: Pet does not exist 73 | 74 | 75 | parameters: 76 | pet_id: 77 | name: pet_id 78 | description: Pet's Unique identifier 79 | in: path 80 | type: string 81 | required: true 82 | pattern: "^[a-zA-Z0-9-]+$" 83 | 84 | definitions: 85 | Pet: 86 | type: object 87 | required: 88 | - name 89 | - animal_type 90 | properties: 91 | id: 92 | type: string 93 | description: Unique identifier 94 | example: "123" 95 | readOnly: true 96 | name: 97 | type: string 98 | description: Pet's name 99 | example: "Susie" 100 | minLength: 1 101 | maxLength: 100 102 | animal_type: 103 | type: string 104 | description: Kind of animal 105 | example: "cat" 106 | minLength: 1 107 | created: 108 | type: string 109 | format: date-time 110 | description: Creation time 111 | example: "2015-07-07T15:49:51.230+02:00" 112 | readOnly: true 113 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "connexion" 3 | version = "3.0.dev0" 4 | description = "Connexion - API first applications with OpenAPI/Swagger" 5 | readme = "README.md" 6 | keywords = ["api", "swagger", "openapi"] 7 | license = "Apache-2.0" 8 | authors = [ 9 | "Robbe Sneyders ", 10 | "Ruwan Lambrichts ", 11 | "Daniel Grossmann-Kavanagh ", 12 | "Henning Jacobs ", 13 | "João Santos ", 14 | ] 15 | maintainers = [ 16 | "Robbe Sneyders ", 17 | "Ruwan Lambrichts ", 18 | ] 19 | repository = "https://github.com/spec-first/connexion" 20 | include = ["*.txt", "*.rst"] 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Intended Audience :: Developers", 24 | "License :: OSI Approved :: Apache Software License", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3 :: Only", 29 | "Programming Language :: Python :: 3.8", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | "Topic :: Internet", 35 | "Topic :: Internet :: WWW/HTTP", 36 | "Topic :: Internet :: WWW/HTTP :: HTTP Servers", 37 | "Topic :: Software Development", 38 | "Topic :: Software Development :: Libraries", 39 | "Topic :: Software Development :: Libraries :: Python Modules", 40 | "Typing :: Typed", 41 | ] 42 | 43 | [tool.poetry.scripts] 44 | connexion = 'connexion.cli:main' 45 | 46 | [tool.poetry.dependencies] 47 | python = '^3.8' 48 | asgiref = ">= 3.4" 49 | httpx = ">= 0.23" 50 | inflection = ">= 0.3.1" 51 | jsonschema = ">=4.17.3" 52 | Jinja2 = ">= 3.0.0" 53 | python-multipart = ">= 0.0.15" 54 | PyYAML = ">= 5.1" 55 | requests = ">= 2.27" 56 | starlette = ">= 0.35" 57 | typing-extensions = ">= 4.6.1" 58 | werkzeug = ">= 2.2.1" 59 | 60 | a2wsgi = { version = ">= 1.7", optional = true } 61 | flask = { version = ">= 2.2", extras = ["async"], optional = true } 62 | swagger-ui-bundle = { version = ">= 1.1.0", optional = true } 63 | uvicorn = { version = ">= 0.17.6", extras = ["standard"], optional = true } 64 | jsf = { version = ">=0.10.0", optional = true } 65 | 66 | [tool.poetry.extras] 67 | flask = ["a2wsgi", "flask"] 68 | swagger-ui = ["swagger-ui-bundle"] 69 | uvicorn = ["uvicorn"] 70 | mock = ["jsf"] 71 | 72 | [tool.poetry.group.tests.dependencies] 73 | pre-commit = "~2.21.0" 74 | pytest = "7.2.1" 75 | pytest-asyncio = "~0.18.3" 76 | pytest-cov = "~2.12.1" 77 | 78 | [tool.poetry.group.docs.dependencies] 79 | sphinx = "5.3.0" 80 | sphinx_copybutton = "0.5.2" 81 | sphinx_design = "0.4.1" 82 | sphinx-rtd-theme = "1.2.0" 83 | sphinxemoji = "0.2.0" 84 | 85 | [build-system] 86 | requires = ["poetry-core>=1.2.0"] 87 | build-backend = "poetry.core.masonry.api" 88 | 89 | [tool.distutils.bdist_wheel] 90 | universal = true 91 | 92 | [tool.pytest.ini_options] 93 | filterwarnings = [ 94 | "ignore::DeprecationWarning:connexion.*:", 95 | "ignore::FutureWarning:connexion.*:", 96 | ] 97 | asyncio_mode = "auto" 98 | 99 | [tool.isort] 100 | profile = "black" 101 | 102 | [tool.coverage.report] 103 | exclude_lines = [ 104 | "pragma: no cover", 105 | "if t.TYPE_CHECKING:", 106 | "@t.overload", 107 | ] 108 | 109 | [[tool.mypy.overrides]] 110 | module = "referencing.jsonschema.*" 111 | follow_imports = "skip" 112 | 113 | [[tool.mypy.overrides]] 114 | module = "referencing._core.*" 115 | follow_imports = "skip" 116 | -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spec-first/connexion/dd79c1146ae31be2145e224552dea95a7473e1fa/tests/api/__init__.py -------------------------------------------------------------------------------- /tests/api/test_bootstrap_multiple_spec.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from conftest import TEST_FOLDER 6 | 7 | SPECS = [ 8 | pytest.param( 9 | [ 10 | {"specification": "swagger_greeting.yaml", "name": "greeting"}, 11 | {"specification": "swagger_bye.yaml", "name": "bye"}, 12 | ], 13 | id="swagger", 14 | ), 15 | pytest.param( 16 | [ 17 | {"specification": "openapi_greeting.yaml", "name": "greeting"}, 18 | {"specification": "openapi_bye.yaml", "name": "bye"}, 19 | ], 20 | id="openapi", 21 | ), 22 | ] 23 | 24 | 25 | @pytest.mark.parametrize("specs", SPECS) 26 | def test_app_with_multiple_definition( 27 | multiple_yaml_same_basepath_dir, specs, app_class 28 | ): 29 | app = app_class( 30 | __name__, 31 | specification_dir=".." 32 | / multiple_yaml_same_basepath_dir.relative_to(TEST_FOLDER), 33 | ) 34 | 35 | for spec in specs: 36 | print(spec) 37 | app.add_api(**spec) 38 | 39 | app_client = app.test_client() 40 | 41 | response = app_client.post("/v1.0/greeting/Igor") 42 | assert response.status_code == 200 43 | print(response.text) 44 | assert response.json()["greeting"] == "Hello Igor" 45 | 46 | response = app_client.get("/v1.0/bye/Musti") 47 | assert response.status_code == 200 48 | assert response.text == "Goodbye Musti" 49 | -------------------------------------------------------------------------------- /tests/api/test_cors.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def test_cors_valid(cors_openapi_app): 5 | app_client = cors_openapi_app.test_client() 6 | origin = "http://localhost" 7 | response = app_client.post("/v1.0/goodday/dan", data={}, headers={"Origin": origin}) 8 | assert response.status_code == 201 9 | assert "Access-Control-Allow-Origin" in response.headers 10 | assert origin == response.headers["Access-Control-Allow-Origin"] 11 | 12 | 13 | def test_cors_invalid(cors_openapi_app): 14 | app_client = cors_openapi_app.test_client() 15 | response = app_client.options( 16 | "/v1.0/goodday/dan", 17 | headers={"Origin": "http://0.0.0.0", "Access-Control-Request-Method": "POST"}, 18 | ) 19 | assert response.status_code == 400 20 | assert "Access-Control-Allow-Origin" not in response.headers 21 | 22 | 23 | def test_cors_validation_error(cors_openapi_app): 24 | app_client = cors_openapi_app.test_client() 25 | origin = "http://localhost" 26 | response = app_client.post( 27 | "/v1.0/body-not-allowed-additional-properties", 28 | data={}, 29 | headers={"Origin": origin}, 30 | ) 31 | assert response.status_code == 400 32 | assert "Access-Control-Allow-Origin" in response.headers 33 | assert origin == response.headers["Access-Control-Allow-Origin"] 34 | 35 | 36 | def test_cors_server_error(cors_openapi_app): 37 | app_client = cors_openapi_app.test_client() 38 | origin = "http://localhost" 39 | response = app_client.post( 40 | "/v1.0/goodday/noheader", data={}, headers={"Origin": origin} 41 | ) 42 | assert response.status_code == 500 43 | assert "Access-Control-Allow-Origin" in response.headers 44 | assert origin == response.headers["Access-Control-Allow-Origin"] 45 | -------------------------------------------------------------------------------- /tests/api/test_headers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def test_headers_jsonifier(simple_app): 5 | app_client = simple_app.test_client() 6 | 7 | response = app_client.post("/v1.0/goodday/dan", data={}) 8 | assert response.status_code == 201 9 | # Default Werkzeug behavior was changed in 2.1 (https://github.com/pallets/werkzeug/issues/2352) 10 | assert response.headers["Location"] in ["http://localhost/my/uri", "/my/uri"] 11 | 12 | 13 | def test_headers_produces(simple_app): 14 | app_client = simple_app.test_client() 15 | 16 | response = app_client.post("/v1.0/goodevening/dan", data={}) 17 | assert response.status_code == 201 18 | # Default Werkzeug behavior was changed in 2.1 (https://github.com/pallets/werkzeug/issues/2352) 19 | assert response.headers["Location"] in ["http://localhost/my/uri", "/my/uri"] 20 | 21 | 22 | def test_header_not_returned(simple_openapi_app): 23 | app_client = simple_openapi_app.test_client() 24 | 25 | response = app_client.post("/v1.0/goodday/noheader", data={}) 26 | assert ( 27 | response.status_code == 500 28 | ) # view_func has not returned what was promised in spec 29 | assert response.headers.get("content-type") == "application/problem+json" 30 | data = response.json() 31 | assert data["type"] == "about:blank" 32 | assert data["title"] == "Internal Server Error" 33 | assert ( 34 | data["detail"] 35 | == "Keys in response header don't match response specification. Difference: location" 36 | ) 37 | assert data["status"] == 500 38 | 39 | 40 | def test_no_content_response_have_headers(simple_app): 41 | app_client = simple_app.test_client() 42 | resp = app_client.get("/v1.0/test-204-with-headers") 43 | assert resp.status_code == 204 44 | assert "X-Something" in resp.headers 45 | 46 | 47 | def test_no_content_object_and_have_headers(simple_app): 48 | app_client = simple_app.test_client() 49 | resp = app_client.get("/v1.0/test-204-with-headers-nocontent-obj") 50 | assert resp.status_code == 204 51 | assert "X-Something" in resp.headers 52 | 53 | 54 | def test_optional_header(simple_openapi_app): 55 | app_client = simple_openapi_app.test_client() 56 | resp = app_client.get("/v1.0/test-optional-headers") 57 | assert resp.status_code == 200 58 | assert "X-Optional-Header" not in resp.headers 59 | -------------------------------------------------------------------------------- /tests/api/test_swagger_ui.py: -------------------------------------------------------------------------------- 1 | def test_simple(swagger_ui_app): 2 | app_client = swagger_ui_app.test_client() 3 | response = app_client.get("/v1.0/spec.json") 4 | assert response.status_code == 200 5 | -------------------------------------------------------------------------------- /tests/api/test_unordered_definition.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def test_app(unordered_definition_app): 5 | app_client = unordered_definition_app.test_client() 6 | response = app_client.get("/v1.0/unordered-params/1?first=first&second=2") 7 | assert response.status_code == 400 8 | response_data = response.json() 9 | assert response_data["detail"].startswith("'first' is not of type 'integer'") 10 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pathlib 3 | 4 | import pytest 5 | from connexion import AsyncApp, FlaskApp 6 | from connexion.resolver import MethodResolver, MethodViewResolver 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | TEST_FOLDER = pathlib.Path(__file__).parent 11 | FIXTURES_FOLDER = TEST_FOLDER / "fixtures" 12 | SPEC_FOLDER = TEST_FOLDER / "fakeapi" 13 | OPENAPI2_SPEC = "swagger.yaml" 14 | OPENAPI3_SPEC = "openapi.yaml" 15 | SPECS = [OPENAPI2_SPEC, OPENAPI3_SPEC] 16 | METHOD_VIEW_RESOLVERS = [MethodResolver, MethodViewResolver] 17 | APP_CLASSES = [FlaskApp, AsyncApp] 18 | 19 | 20 | @pytest.fixture 21 | def simple_api_spec_dir(): 22 | return FIXTURES_FOLDER / "simple" 23 | 24 | 25 | @pytest.fixture 26 | def problem_api_spec_dir(): 27 | return FIXTURES_FOLDER / "problem" 28 | 29 | 30 | @pytest.fixture 31 | def secure_api_spec_dir(): 32 | return FIXTURES_FOLDER / "secure_api" 33 | 34 | 35 | @pytest.fixture 36 | def default_param_error_spec_dir(): 37 | return FIXTURES_FOLDER / "default_param_error" 38 | 39 | 40 | @pytest.fixture 41 | def json_validation_spec_dir(): 42 | return FIXTURES_FOLDER / "json_validation" 43 | 44 | 45 | @pytest.fixture 46 | def multiple_yaml_same_basepath_dir(): 47 | return FIXTURES_FOLDER / "multiple_yaml_same_basepath" 48 | 49 | 50 | @pytest.fixture(scope="session") 51 | def json_datetime_dir(): 52 | return FIXTURES_FOLDER / "datetime_support" 53 | 54 | 55 | @pytest.fixture(scope="session") 56 | def relative_refs(): 57 | return FIXTURES_FOLDER / "relative_refs" 58 | 59 | 60 | @pytest.fixture(scope="session", params=SPECS) 61 | def spec(request): 62 | return request.param 63 | 64 | 65 | @pytest.fixture(scope="session", params=METHOD_VIEW_RESOLVERS) 66 | def method_view_resolver(request): 67 | return request.param 68 | 69 | 70 | @pytest.fixture(scope="session", params=APP_CLASSES) 71 | def app_class(request): 72 | return request.param 73 | 74 | 75 | def build_app_from_fixture( 76 | api_spec_folder, *, app_class, spec_file, middlewares=None, **kwargs 77 | ): 78 | cnx_app = app_class( 79 | __name__, 80 | specification_dir=FIXTURES_FOLDER / api_spec_folder, 81 | middlewares=middlewares, 82 | ) 83 | 84 | cnx_app.add_api(spec_file, **kwargs) 85 | cnx_app._spec_file = spec_file 86 | return cnx_app 87 | -------------------------------------------------------------------------------- /tests/decorators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spec-first/connexion/dd79c1146ae31be2145e224552dea95a7473e1fa/tests/decorators/__init__.py -------------------------------------------------------------------------------- /tests/decorators/test_parameter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest.mock import AsyncMock, MagicMock 3 | 4 | import pytest 5 | from connexion.decorators.parameter import ( 6 | AsyncParameterDecorator, 7 | SyncParameterDecorator, 8 | pythonic, 9 | ) 10 | from connexion.frameworks.flask import Flask as FlaskFramework 11 | from connexion.frameworks.starlette import Starlette as StarletteFramework 12 | from connexion.testing import TestContext 13 | 14 | 15 | def test_sync_injection(): 16 | request = MagicMock(name="request") 17 | request.path_params = {"p1": "123"} 18 | request.get_body.return_value = {} 19 | 20 | func = MagicMock() 21 | 22 | def handler(**kwargs): 23 | func(**kwargs) 24 | 25 | operation = MagicMock(name="operation") 26 | operation.is_request_body_defined = False 27 | operation.body_name = lambda _: "body" 28 | 29 | with TestContext(operation=operation): 30 | parameter_decorator = SyncParameterDecorator(framework=FlaskFramework) 31 | decorated_handler = parameter_decorator(handler) 32 | decorated_handler(request) 33 | func.assert_called_with(p1="123") 34 | 35 | 36 | @pytest.mark.skipif( 37 | sys.version_info < (3, 8), reason="AsyncMock only available from 3.8." 38 | ) 39 | async def test_async_injection(): 40 | request = AsyncMock(name="request") 41 | request.path_params = {"p1": "123"} 42 | request.get_body.return_value = {} 43 | request.files.return_value = {} 44 | 45 | func = MagicMock() 46 | 47 | async def handler(**kwargs): 48 | func(**kwargs) 49 | 50 | operation = MagicMock(name="operation") 51 | operation.is_request_body_defined = False 52 | operation.body_name = lambda _: "body" 53 | 54 | with TestContext(operation=operation): 55 | parameter_decorator = AsyncParameterDecorator(framework=StarletteFramework) 56 | decorated_handler = parameter_decorator(handler) 57 | await decorated_handler(request) 58 | func.assert_called_with(p1="123") 59 | 60 | 61 | def test_sync_injection_with_context(): 62 | request = MagicMock(name="request") 63 | request.path_params = {"p1": "123"} 64 | request.get_body.return_value = {} 65 | 66 | func = MagicMock() 67 | 68 | def handler(context_, **kwargs): 69 | func(context_, **kwargs) 70 | 71 | context = {"test": "success"} 72 | 73 | operation = MagicMock(name="operation") 74 | operation.is_request_body_defined = False 75 | operation.body_name = lambda _: "body" 76 | 77 | with TestContext(context=context, operation=operation): 78 | parameter_decorator = SyncParameterDecorator(framework=FlaskFramework) 79 | decorated_handler = parameter_decorator(handler) 80 | decorated_handler(request) 81 | func.assert_called_with(context, p1="123", test="success") 82 | 83 | 84 | @pytest.mark.skipif( 85 | sys.version_info < (3, 8), reason="AsyncMock only available from 3.8." 86 | ) 87 | async def test_async_injection_with_context(): 88 | request = AsyncMock(name="request") 89 | request.path_params = {"p1": "123"} 90 | request.get_body.return_value = {} 91 | request.files.return_value = {} 92 | 93 | func = MagicMock() 94 | 95 | async def handler(context_, **kwargs): 96 | func(context_, **kwargs) 97 | 98 | context = {"test": "success"} 99 | 100 | operation = MagicMock(name="operation") 101 | operation.is_request_body_defined = False 102 | operation.body_name = lambda _: "body" 103 | 104 | with TestContext(context=context, operation=operation): 105 | parameter_decorator = AsyncParameterDecorator(framework=StarletteFramework) 106 | decorated_handler = parameter_decorator(handler) 107 | await decorated_handler(request) 108 | func.assert_called_with(context, p1="123", test="success") 109 | 110 | 111 | def test_pythonic_params(): 112 | assert pythonic("orderBy[eq]") == "order_by_eq" 113 | assert pythonic("ids[]") == "ids" 114 | -------------------------------------------------------------------------------- /tests/fakeapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .example_method_view import PetsView 2 | 3 | 4 | def get(): 5 | return "" 6 | -------------------------------------------------------------------------------- /tests/fakeapi/auth.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from connexion.exceptions import OAuthProblem 4 | 5 | 6 | def fake_basic_auth(username, password, required_scopes=None): 7 | if username == password: 8 | return {"uid": username} 9 | return None 10 | 11 | 12 | def fake_json_auth(token, required_scopes=None): 13 | try: 14 | return json.loads(token) 15 | except ValueError: 16 | return None 17 | 18 | 19 | async def async_auth_exception(token, required_scopes=None, request=None): 20 | raise OAuthProblem 21 | -------------------------------------------------------------------------------- /tests/fakeapi/example_method_class.py: -------------------------------------------------------------------------------- 1 | class PetsView: 2 | 3 | mycontent = "demonstrate return from MethodView class" 4 | 5 | def get(self, **kwargs): 6 | if kwargs: 7 | kwargs.update({"name": "get"}) 8 | return kwargs 9 | else: 10 | return [{"name": "get"}] 11 | 12 | def search(self): 13 | return [{"name": "search"}] 14 | 15 | def post(self, **kwargs): 16 | kwargs.update({"name": "post"}) 17 | return kwargs, 201 18 | 19 | def put(self, *args, **kwargs): 20 | kwargs.update({"name": "put"}) 21 | return kwargs, 201 22 | 23 | def delete(self, **kwargs): 24 | return 201 25 | 26 | # Test that operation_id can still override resolver 27 | 28 | def api_list(self): 29 | return "api_list" 30 | 31 | def post_greeting(self): 32 | return "post_greeting" 33 | -------------------------------------------------------------------------------- /tests/fakeapi/example_method_view.py: -------------------------------------------------------------------------------- 1 | from flask.views import MethodView 2 | 3 | 4 | class PetsView(MethodView): 5 | 6 | mycontent = "demonstrate return from MethodView class" 7 | 8 | def get(self, **kwargs): 9 | if kwargs: 10 | kwargs.update({"name": "get"}) 11 | return kwargs 12 | else: 13 | return [{"name": "get"}] 14 | 15 | def search(self): 16 | return [{"name": "search"}] 17 | 18 | def post(self, **kwargs): 19 | kwargs.update({"name": "post"}) 20 | return kwargs, 201 21 | 22 | def put(self, *args, **kwargs): 23 | kwargs.update({"name": "put"}) 24 | return kwargs, 201 25 | 26 | # Test that operation_id can still override resolver 27 | 28 | def api_list(self): 29 | return "api_list" 30 | 31 | def post_greeting(self): 32 | return "post_greeting" 33 | -------------------------------------------------------------------------------- /tests/fakeapi/foo_bar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | def search(): 5 | return "" 6 | -------------------------------------------------------------------------------- /tests/fakeapi/hello/world.py: -------------------------------------------------------------------------------- 1 | def search(id): 2 | return "" 3 | 4 | 5 | def get(id): 6 | return "" 7 | -------------------------------------------------------------------------------- /tests/fakeapi/module_with_error.py: -------------------------------------------------------------------------------- 1 | # This is a test file, please do not delete. 2 | # It is used by the test: 3 | # - `test_operation.py:test_invalid_operation_does_stop_application_to_setup` 4 | # - `test_api.py:test_invalid_operation_does_stop_application_to_setup` 5 | # - `test_api.py:test_invalid_operation_does_not_stop_application_in_debug_mode` 6 | from foo.bar import foobar # noqa 7 | -------------------------------------------------------------------------------- /tests/fakeapi/module_with_exception.py: -------------------------------------------------------------------------------- 1 | # This is a test file, please do not delete. 2 | # It is used by the test: 3 | # - `test_operation.py:test_invalid_operation_does_stop_application_to_setup` 4 | # - `test_api.py:test_invalid_operation_does_stop_application_to_setup` 5 | # - `test_api.py:test_invalid_operation_does_not_stop_application_in_debug_mode` 6 | raise ValueError("Forced exception!") 7 | -------------------------------------------------------------------------------- /tests/fakeapi/snake_case.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | def get_path_snake(some_id): 3 | data = {"SomeId": some_id} 4 | return data 5 | 6 | 7 | def get_path_shadow(id_): 8 | data = {"id": id_} 9 | return data 10 | 11 | 12 | def get_query_snake(some_id): 13 | data = {"someId": some_id} 14 | return data 15 | 16 | 17 | def get_query_shadow(list_): 18 | data = {"list": list_} 19 | return data 20 | 21 | 22 | def get_camelcase(truthiness, order_by=None): 23 | data = {"truthiness": truthiness, "order_by": order_by} 24 | return data 25 | 26 | 27 | def post_path_snake(some_id, some_other_id): 28 | data = {"SomeId": some_id, "SomeOtherId": some_other_id} 29 | return data 30 | 31 | 32 | def post_path_shadow(id_, round_): 33 | data = {"id": id_, "reduce": round_} 34 | return data 35 | 36 | 37 | def post_query_snake(some_id, some_other_id): 38 | data = {"someId": some_id, "someOtherId": some_other_id} 39 | return data 40 | 41 | 42 | def post_query_shadow(id_, class_, next_): 43 | data = {"id": id_, "class": class_, "next": next_} 44 | return data 45 | -------------------------------------------------------------------------------- /tests/fixtures/bad_operations/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | /welcome: 7 | get: 8 | operationId: no.module.or.function 9 | responses: 10 | '200': 11 | description: greeting response 12 | content: 13 | '*/*': 14 | schema: 15 | type: object 16 | put: 17 | responses: 18 | '200': 19 | description: greeting response 20 | content: 21 | '*/*': 22 | schema: 23 | type: object 24 | post: 25 | operationId: fakeapi.module_with_error.something 26 | responses: 27 | '200': 28 | description: greeting response 29 | content: 30 | '*/*': 31 | schema: 32 | type: object 33 | servers: 34 | - url: /v1.0 35 | -------------------------------------------------------------------------------- /tests/fixtures/bad_operations/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /welcome: 11 | get: 12 | operationId: no.module.or.function 13 | responses: 14 | '200': 15 | description: greeting response 16 | schema: 17 | type: object 18 | put: 19 | # operationId: XXX completely missing 20 | responses: 21 | '200': 22 | description: greeting response 23 | schema: 24 | type: object 25 | post: 26 | operationId: fakeapi.module_with_error.something 27 | responses: 28 | '200': 29 | description: greeting response 30 | schema: 31 | type: object 32 | -------------------------------------------------------------------------------- /tests/fixtures/bad_specs/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | /welcome: 7 | get: 8 | operationId: fakeapi.foo_bar.search 9 | parameters: 10 | - name: foo 11 | in: query 12 | schema: 13 | type: integer 14 | default: somestring 15 | responses: 16 | '200': 17 | description: search 18 | content: 19 | '*/*': 20 | schema: 21 | type: object 22 | servers: 23 | - url: /v1.0 24 | -------------------------------------------------------------------------------- /tests/fixtures/bad_specs/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /welcome: 11 | get: 12 | operationId: fakeapi.foo_bar.search 13 | parameters: 14 | # The default below validates, but is obviously the wrong type 15 | - name: foo 16 | in: query 17 | type: integer 18 | default: somestring 19 | responses: 20 | '200': 21 | description: search 22 | schema: 23 | type: object 24 | -------------------------------------------------------------------------------- /tests/fixtures/datetime_support/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.1" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | servers: 7 | - url: http://localhost:8080/v1.0 8 | 9 | paths: 10 | /datetime: 11 | get: 12 | summary: Generate data with date time 13 | operationId: fakeapi.hello.get_datetime 14 | responses: 15 | '200': 16 | description: date time example 17 | content: 18 | application/json: 19 | schema: 20 | type: object 21 | properties: 22 | value: 23 | type: string 24 | format: date-time 25 | example: 26 | value: 2000-01-23T04:56:07.000008+00:00 27 | /date: 28 | get: 29 | summary: Generate data with date 30 | operationId: fakeapi.hello.get_date 31 | responses: 32 | '200': 33 | description: date example 34 | content: 35 | application/json: 36 | schema: 37 | type: object 38 | properties: 39 | value: 40 | type: string 41 | format: date 42 | example: 43 | value: 2000-01-23 44 | /uuid: 45 | get: 46 | summary: Generate data with uuid 47 | operationId: fakeapi.hello.get_uuid 48 | responses: 49 | '200': 50 | description: uuid example 51 | content: 52 | application/json: 53 | schema: 54 | type: object 55 | properties: 56 | value: 57 | type: string 58 | format: uuid 59 | example: 60 | value: 'a7b8869c-5f24-4ce0-a5d1-3e44c3663aa9' 61 | -------------------------------------------------------------------------------- /tests/fixtures/datetime_support/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /datetime: 11 | get: 12 | operationId: fakeapi.hello.get_datetime 13 | responses: 14 | '200': 15 | description: date time example 16 | schema: 17 | type: object 18 | properties: 19 | value: 20 | type: string 21 | format: date-time 22 | example: 23 | value: 2000-01-23T04:56:07.000008+00:00 24 | /date: 25 | get: 26 | operationId: fakeapi.hello.get_date 27 | responses: 28 | '200': 29 | description: date example 30 | schema: 31 | type: object 32 | properties: 33 | value: 34 | type: string 35 | format: date 36 | example: 37 | value: 2000-01-23 38 | /uuid: 39 | get: 40 | summary: Generate data with uuid 41 | operationId: fakeapi.hello.get_uuid 42 | responses: 43 | '200': 44 | description: uuid example 45 | schema: 46 | type: object 47 | properties: 48 | value: 49 | type: string 50 | format: uuid 51 | example: 52 | value: 'a7b8869c-5f24-4ce0-a5d1-3e44c3663aa9' 53 | -------------------------------------------------------------------------------- /tests/fixtures/default_param_error/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | /default-param-query-does-not-match-type: 7 | get: 8 | summary: Default value does not match the param type 9 | operationId: fakeapi.hello.test_default_mismatch_definition 10 | responses: 11 | '200': 12 | description: OK 13 | parameters: 14 | - name: age 15 | in: query 16 | description: Simple age 17 | schema: 18 | type: integer 19 | default: 'error' 20 | servers: 21 | - url: /v1.0 22 | -------------------------------------------------------------------------------- /tests/fixtures/default_param_error/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /default-param-query-does-not-match-type: 11 | get: 12 | summary: Default value does not match the param type 13 | operationId: fakeapi.hello.test_default_mismatch_definition 14 | responses: 15 | '200': 16 | description: OK 17 | parameters: 18 | - name: age 19 | in: query 20 | type: integer 21 | description: Simple age 22 | default: 'error' 23 | -------------------------------------------------------------------------------- /tests/fixtures/invalid_schema/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "Bar" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /foobar: 11 | post: 12 | invalidValue 13 | -------------------------------------------------------------------------------- /tests/fixtures/json_validation/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | servers: 8 | - url: /v1.0 9 | 10 | components: 11 | schemas: 12 | User: 13 | type: object 14 | required: 15 | - name 16 | properties: 17 | user_id: 18 | type: integer 19 | readOnly: true 20 | name: 21 | type: string 22 | password: 23 | type: string 24 | writeOnly: true 25 | human: 26 | type: boolean 27 | default: true 28 | X: 29 | type: object 30 | properties: 31 | name: 32 | type: string 33 | age: 34 | type: integer 35 | 36 | paths: 37 | /minlength: 38 | post: 39 | operationId: fakeapi.hello.post 40 | requestBody: 41 | content: 42 | application/json: 43 | schema: 44 | type: object 45 | properties: 46 | foo: 47 | type: string 48 | responses: 49 | '200': 50 | description: Success 51 | 52 | /user: 53 | get: 54 | operationId: fakeapi.hello.get_user 55 | responses: 56 | '200': 57 | description: User object 58 | content: 59 | application/json: 60 | schema: 61 | $ref: '#/components/schemas/User' 62 | post: 63 | operationId: fakeapi.hello.post_user 64 | requestBody: 65 | content: 66 | application/json: 67 | schema: 68 | $ref: '#/components/schemas/User' 69 | responses: 70 | '200': 71 | description: User object 72 | content: 73 | application/json: 74 | schema: 75 | $ref: '#/components/schemas/User' 76 | /user_with_password: 77 | get: 78 | operationId: fakeapi.hello.get_user_with_password 79 | responses: 80 | '200': 81 | description: User object 82 | content: 83 | application/json: 84 | schema: 85 | $ref: '#/components/schemas/User' 86 | 87 | /nullable_default: 88 | get: 89 | operationId: fakeapi.hello.nullable_default 90 | parameters: 91 | - name: test 92 | in: query 93 | schema: 94 | type: string 95 | nullable: true 96 | default: null 97 | responses: 98 | '204': 99 | description: OK 100 | 101 | /multipart_form_json: 102 | post: 103 | operationId: fakeapi.hello.post_multipart_form 104 | requestBody: 105 | required: true 106 | content: 107 | multipart/form-data: 108 | schema: 109 | type: object 110 | properties: 111 | x: 112 | $ref: "#/components/schemas/X" 113 | encoding: 114 | x: 115 | contentType: "application/json" 116 | responses: 117 | '200': 118 | description: Modified Echo 119 | content: 120 | application/json: 121 | schema: 122 | $ref: "#/components/schemas/X" 123 | 124 | 125 | /multipart_form_json_array: 126 | post: 127 | operationId: fakeapi.hello.post_multipart_form_array 128 | requestBody: 129 | required: true 130 | content: 131 | multipart/form-data: 132 | schema: 133 | type: object 134 | properties: 135 | x: 136 | type: array 137 | items: 138 | $ref: "#/components/schemas/X" 139 | encoding: 140 | x: 141 | contentType: "application/json" 142 | responses: 143 | '200': 144 | description: Modified Echo 145 | content: 146 | application/json: 147 | schema: 148 | type: array 149 | items: 150 | $ref: "#/components/schemas/X" 151 | -------------------------------------------------------------------------------- /tests/fixtures/json_validation/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | definitions: 10 | User: 11 | type: object 12 | required: 13 | - name 14 | properties: 15 | user_id: 16 | type: integer 17 | readOnly: true 18 | name: 19 | type: string 20 | password: 21 | type: string 22 | x-writeOnly: true 23 | human: 24 | type: boolean 25 | default: true 26 | paths: 27 | /minlength: 28 | post: 29 | operationId: fakeapi.hello.post 30 | parameters: 31 | - name: body 32 | in: body 33 | required: true 34 | schema: 35 | type: object 36 | properties: 37 | foo: 38 | type: string 39 | responses: 40 | '200': 41 | description: Success 42 | 43 | /user: 44 | get: 45 | operationId: fakeapi.hello.get_user 46 | responses: 47 | '200': 48 | description: User object 49 | schema: 50 | $ref: '#/definitions/User' 51 | post: 52 | operationId: fakeapi.hello.post_user 53 | parameters: 54 | - name: body 55 | in: body 56 | required: true 57 | schema: 58 | $ref: '#/definitions/User' 59 | responses: 60 | '200': 61 | description: User object 62 | schema: 63 | $ref: '#/definitions/User' 64 | /user_with_password: 65 | get: 66 | operationId: fakeapi.hello.get_user_with_password 67 | responses: 68 | '200': 69 | description: User object 70 | schema: 71 | $ref: '#/definitions/User' 72 | 73 | /nullable_default: 74 | get: 75 | operationId: fakeapi.hello.nullable_default 76 | parameters: 77 | - name: test 78 | in: query 79 | type: string 80 | x-nullable: true 81 | default: null 82 | responses: 83 | 204: 84 | description: OK 85 | -------------------------------------------------------------------------------- /tests/fixtures/method_view/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /pets: 11 | get: 12 | summary: List all pets 13 | tags: 14 | - pets 15 | parameters: 16 | - name: limit 17 | in: query 18 | description: How many items to return at one time (max 100) 19 | required: false 20 | type: integer 21 | format: int32 22 | responses: 23 | '200': 24 | description: An paged array of pets 25 | schema: 26 | $ref: "#/definitions/Pets" 27 | default: 28 | description: unexpected error 29 | schema: 30 | $ref: "#/definitions/Error" 31 | post: 32 | summary: Create a pet 33 | tags: 34 | - pets 35 | parameters: 36 | - name: body 37 | in: body 38 | required: true 39 | description: Pet to add to the system 40 | schema: 41 | $ref: "#/definitions/Pet" 42 | responses: 43 | '201': 44 | description: Pet record interpreted by backend 45 | schema: 46 | $ref: "#/definitions/Pet" 47 | default: 48 | description: unexpected error 49 | schema: 50 | $ref: "#/definitions/Error" 51 | 52 | '/pets/{petId}': 53 | get: 54 | summary: Info for a specific pet 55 | tags: 56 | - pets 57 | parameters: 58 | - name: petId 59 | in: path 60 | required: true 61 | description: The id of the pet to retrieve 62 | type: integer 63 | responses: 64 | '200': 65 | description: Expected response to a valid request 66 | schema: 67 | $ref: "#/definitions/Pet" 68 | default: 69 | description: unexpected error 70 | schema: 71 | $ref: "#/definitions/Error" 72 | 73 | put: 74 | summary: Update a pet 75 | tags: 76 | - pets 77 | parameters: 78 | - name: petId 79 | in: path 80 | required: true 81 | description: The id of the pet to update 82 | type: integer 83 | - name: body 84 | in: body 85 | required: true 86 | description: Pet to add to the system 87 | schema: 88 | $ref: "#/definitions/Pet" 89 | responses: 90 | '201': 91 | description: Pet record interpreted by backend 92 | schema: 93 | $ref: "#/definitions/Pet" 94 | default: 95 | description: unexpected error 96 | schema: 97 | $ref: "#/definitions/Error" 98 | delete: 99 | summary: Update a pet 100 | tags: 101 | - pets 102 | parameters: 103 | - name: petId 104 | in: path 105 | required: true 106 | description: The id of the pet to update 107 | type: integer 108 | responses: 109 | '204': 110 | description: Null response 111 | default: 112 | description: unexpected error 113 | schema: 114 | $ref: "#/definitions/Error" 115 | 116 | 117 | definitions: 118 | Pet: 119 | required: 120 | - name 121 | properties: 122 | name: 123 | type: string 124 | example: fluffy 125 | tag: 126 | type: string 127 | example: red 128 | id: 129 | type: integer 130 | format: int64 131 | readOnly: true 132 | example: 1 133 | last_updated: 134 | type: string 135 | readOnly: true 136 | example: 2019-01-16T23:52:54.309102Z 137 | 138 | Pets: 139 | type: array 140 | items: 141 | $ref: "#/definitions/Pet" 142 | 143 | Error: 144 | required: 145 | - code 146 | - message 147 | properties: 148 | code: 149 | type: integer 150 | format: int32 151 | message: 152 | type: string 153 | -------------------------------------------------------------------------------- /tests/fixtures/missing_implementation/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Testing API 4 | version: '1.0' 5 | paths: 6 | /operation-not-implemented: 7 | get: 8 | summary: Operation function does not exist. 9 | operationId: api.this_function_does_not_exist 10 | responses: 11 | '200': 12 | description: OK 13 | servers: 14 | - url: /testing 15 | -------------------------------------------------------------------------------- /tests/fixtures/missing_implementation/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "Testing API" 5 | version: "1.0" 6 | 7 | basePath: "/testing" 8 | 9 | paths: 10 | /operation-not-implemented: 11 | get: 12 | summary: Operation function does not exist. 13 | operationId: api.this_function_does_not_exist 14 | responses: 15 | '200': 16 | description: OK 17 | -------------------------------------------------------------------------------- /tests/fixtures/missing_op_id/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | /welcome: 7 | put: 8 | responses: 9 | '200': 10 | description: greeting response 11 | content: 12 | '*/*': 13 | schema: 14 | type: object 15 | servers: 16 | - url: /v1.0 17 | -------------------------------------------------------------------------------- /tests/fixtures/missing_op_id/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /welcome: 11 | put: 12 | # operationId: XXX completely missing 13 | responses: 14 | '200': 15 | description: greeting response 16 | schema: 17 | type: object 18 | -------------------------------------------------------------------------------- /tests/fixtures/module_does_not_exist/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Not Exist API 4 | version: '1.0' 5 | paths: 6 | /module-not-implemented/{some_path}: 7 | get: 8 | summary: Operation function does not exist. 9 | operationId: m.module_does_not_exist 10 | parameters: 11 | - in: path 12 | name: some_path 13 | schema: 14 | type: string 15 | responses: 16 | '200': 17 | description: OK 18 | servers: 19 | - url: /na 20 | -------------------------------------------------------------------------------- /tests/fixtures/module_does_not_exist/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "Not Exist API" 5 | version: "1.0" 6 | 7 | basePath: '/na' 8 | 9 | paths: 10 | /module-not-implemented/{some_path}: 11 | get: 12 | summary: Operation function does not exist. 13 | operationId: m.module_does_not_exist 14 | parameters: 15 | - name: some_path 16 | in: path 17 | required: true 18 | type: string 19 | responses: 20 | '200': 21 | description: OK 22 | -------------------------------------------------------------------------------- /tests/fixtures/module_not_implemented/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | /welcome: 7 | get: 8 | operationId: no.module.or.function 9 | responses: 10 | '200': 11 | description: greeting response 12 | content: 13 | '*/*': 14 | schema: 15 | type: object 16 | servers: 17 | - url: /v1.0 18 | -------------------------------------------------------------------------------- /tests/fixtures/module_not_implemented/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /welcome: 11 | get: 12 | operationId: no.module.or.function 13 | responses: 14 | '200': 15 | description: greeting response 16 | schema: 17 | type: object 18 | -------------------------------------------------------------------------------- /tests/fixtures/multiple_yaml_same_basepath/openapi_bye.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | '/bye/{name}': 7 | get: 8 | summary: Generate goodbye 9 | description: Generates a goodbye message. 10 | operationId: fakeapi.hello.get_bye 11 | responses: 12 | '200': 13 | description: goodbye response 14 | content: 15 | text/plain: 16 | schema: 17 | type: string 18 | default: 19 | description: unexpected error 20 | parameters: 21 | - name: name 22 | in: path 23 | description: Name of the person to say bye. 24 | required: true 25 | schema: 26 | type: string 27 | servers: 28 | - url: /v1.0 29 | -------------------------------------------------------------------------------- /tests/fixtures/multiple_yaml_same_basepath/openapi_greeting.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | '/greeting/{name}': 7 | post: 8 | summary: Generate greeting 9 | description: Generates a greeting message. 10 | operationId: fakeapi.hello.post_greeting 11 | responses: 12 | '200': 13 | description: greeting response 14 | content: 15 | 'application/json': 16 | schema: 17 | type: object 18 | parameters: 19 | - name: name 20 | in: path 21 | description: Name of the person to greet. 22 | required: true 23 | schema: 24 | type: string 25 | 26 | 27 | servers: 28 | - url: /v1.0 29 | -------------------------------------------------------------------------------- /tests/fixtures/multiple_yaml_same_basepath/swagger_bye.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /bye/{name}: 11 | get: 12 | summary: Generate goodbye 13 | description: Generates a goodbye message. 14 | operationId: fakeapi.hello.get_bye 15 | produces: 16 | - text/plain 17 | responses: 18 | '200': 19 | description: goodbye response 20 | schema: 21 | type: string 22 | default: 23 | description: "unexpected error" 24 | parameters: 25 | - name: name 26 | in: path 27 | description: Name of the person to say bye. 28 | required: true 29 | type: string 30 | -------------------------------------------------------------------------------- /tests/fixtures/multiple_yaml_same_basepath/swagger_greeting.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /greeting/{name}: 11 | post: 12 | summary: Generate greeting 13 | description: Generates a greeting message. 14 | operationId: fakeapi.hello.post_greeting 15 | responses: 16 | '200': 17 | description: greeting response 18 | schema: 19 | type: object 20 | parameters: 21 | - name: name 22 | in: path 23 | description: Name of the person to greet. 24 | required: true 25 | type: string -------------------------------------------------------------------------------- /tests/fixtures/op_error_api/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | /welcome: 7 | get: 8 | operationId: fakeapi.module_with_error.something 9 | responses: 10 | '200': 11 | description: greeting response 12 | content: 13 | '*/*': 14 | schema: 15 | type: object 16 | servers: 17 | - url: /v1.0 18 | -------------------------------------------------------------------------------- /tests/fixtures/op_error_api/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /welcome: 11 | get: 12 | operationId: fakeapi.module_with_error.something 13 | responses: 14 | '200': 15 | description: greeting response 16 | schema: 17 | type: object 18 | -------------------------------------------------------------------------------- /tests/fixtures/problem/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | '/greeting/{name}': 7 | post: 8 | summary: Generate greeting 9 | description: Generates a greeting message. 10 | operationId: fakeapi.hello.post_greeting 11 | responses: 12 | '200': 13 | description: greeting response 14 | content: 15 | '*/*': 16 | schema: 17 | type: object 18 | parameters: 19 | - name: name 20 | in: path 21 | description: Name of the person to greet. 22 | required: true 23 | schema: 24 | type: string 25 | /except: 26 | get: 27 | summary: Fails badly 28 | description: Fails badly 29 | operationId: fakeapi.hello.internal_error 30 | responses: 31 | '200': 32 | description: goodbye response 33 | content: 34 | text/plain: 35 | schema: 36 | type: string 37 | /problem: 38 | get: 39 | summary: Fails 40 | description: Fails 41 | operationId: fakeapi.hello.with_problem 42 | responses: 43 | '200': 44 | description: goodbye response 45 | content: 46 | application/json: 47 | schema: 48 | type: string 49 | /other_problem: 50 | get: 51 | summary: Fails 52 | description: Fails 53 | operationId: fakeapi.hello.with_problem_txt 54 | responses: 55 | '200': 56 | description: goodbye response 57 | content: 58 | text/plain: 59 | schema: 60 | type: string 61 | /json_response_with_undefined_value_to_serialize: 62 | get: 63 | description: Will fail 64 | operationId: fakeapi.hello.get_invalid_response 65 | responses: 66 | '200': 67 | description: Never happens 68 | /customized_problem_response: 69 | get: 70 | description: Custom problem response 71 | operationId: fakeapi.hello.get_custom_problem_response 72 | responses: 73 | '200': 74 | description: Custom problem response 75 | /problem_exception_with_extra_args: 76 | get: 77 | description: Using problem as exception 78 | operationId: fakeapi.hello.throw_problem_exception 79 | responses: 80 | '200': 81 | description: Problem exception 82 | /post_wrong_content_type: 83 | post: 84 | description: Unsupported media type 85 | operationId: fakeapi.hello.post_wrong_content_type 86 | requestBody: 87 | content: 88 | application/json: 89 | schema: 90 | type: object 91 | responses: 92 | '200': 93 | description: OK 94 | servers: 95 | - url: /v1.0 96 | components: 97 | securitySchemes: 98 | oauth: 99 | type: oauth2 100 | x-tokenInfoUrl: 'https://oauth.example/token_info' 101 | flows: 102 | password: 103 | tokenUrl: 'https://oauth.example/token' 104 | scopes: 105 | myscope: can do stuff 106 | -------------------------------------------------------------------------------- /tests/fixtures/problem/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | securityDefinitions: 10 | oauth: 11 | type: oauth2 12 | flow: password 13 | tokenUrl: https://oauth.example/token 14 | x-tokenInfoUrl: https://oauth.example/token_info 15 | scopes: 16 | myscope: can do stuff 17 | 18 | paths: 19 | /greeting/{name}: 20 | post: 21 | summary: Generate greeting 22 | description: Generates a greeting message. 23 | operationId: fakeapi.hello.post_greeting 24 | responses: 25 | '200': 26 | description: greeting response 27 | schema: 28 | type: object 29 | parameters: 30 | - name: name 31 | in: path 32 | description: Name of the person to greet. 33 | required: true 34 | type: string 35 | 36 | /except: 37 | get: 38 | summary: Fails badly 39 | description: Fails badly 40 | operationId: fakeapi.hello.internal_error 41 | produces: 42 | - text/plain 43 | responses: 44 | '200': 45 | description: goodbye response 46 | schema: 47 | type: string 48 | 49 | /problem: 50 | get: 51 | summary: Fails 52 | description: Fails 53 | operationId: fakeapi.hello.with_problem 54 | produces: 55 | - application/json 56 | responses: 57 | '200': 58 | description: goodbye response 59 | schema: 60 | type: string 61 | 62 | /other_problem: 63 | get: 64 | summary: Fails 65 | description: Fails 66 | operationId: fakeapi.hello.with_problem_txt 67 | produces: 68 | - text/plain 69 | responses: 70 | '200': 71 | description: goodbye response 72 | schema: 73 | type: string 74 | 75 | /json_response_with_undefined_value_to_serialize: 76 | get: 77 | description: Will fail 78 | operationId: fakeapi.hello.get_invalid_response 79 | produces: 80 | - application/json 81 | responses: 82 | '200': 83 | description: Never happens 84 | 85 | /customized_problem_response: 86 | get: 87 | description: Custom problem response 88 | operationId: fakeapi.hello.get_custom_problem_response 89 | produces: 90 | - application/json 91 | responses: 92 | '200': 93 | description: Custom problem response 94 | 95 | /problem_exception_with_extra_args: 96 | get: 97 | description: Using problem as exception 98 | operationId: fakeapi.hello.throw_problem_exception 99 | produces: 100 | - application/json 101 | responses: 102 | '200': 103 | description: Problem exception 104 | 105 | /post_wrong_content_type: 106 | post: 107 | description: Unsupported media type 108 | operationId: fakeapi.hello.post_wrong_content_type 109 | consumes: 110 | - application/json 111 | parameters: 112 | - in: body 113 | name: body 114 | description: The request body 115 | schema: 116 | type: object 117 | responses: 118 | '200': 119 | description: OK 120 | -------------------------------------------------------------------------------- /tests/fixtures/relative_refs/components.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | Pet: 4 | required: 5 | - name 6 | properties: 7 | name: 8 | type: string 9 | example: fluffy 10 | tag: 11 | type: string 12 | example: red 13 | id: 14 | type: integer 15 | format: int64 16 | readOnly: true 17 | example: 1 18 | last_updated: 19 | type: string 20 | readOnly: true 21 | example: 2019-01-16T23:52:54.309102Z 22 | 23 | Pets: 24 | type: array 25 | items: 26 | $ref: "#/components/schemas/Pet" 27 | 28 | Error: 29 | properties: 30 | code: 31 | type: integer 32 | format: int32 33 | message: 34 | type: string 35 | required: 36 | - code 37 | - message 38 | -------------------------------------------------------------------------------- /tests/fixtures/relative_refs/definitions.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | Pet: 3 | type: object 4 | properties: 5 | id: 6 | type: integer 7 | format: int64 8 | name: 9 | type: string 10 | registered: 11 | type: string 12 | format: date-time 13 | 14 | Pets: 15 | type: array 16 | items: 17 | $ref: "#/definitions/Pet" 18 | 19 | Error: 20 | type: object 21 | properties: 22 | code: 23 | type: integer 24 | format: int32 25 | message: 26 | type: string 27 | required: 28 | - code 29 | - message -------------------------------------------------------------------------------- /tests/fixtures/relative_refs/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | 3 | info: 4 | version: 1.0.0 5 | title: Swagger Petstore 6 | license: 7 | name: MIT 8 | 9 | servers: 10 | - url: /openapi 11 | 12 | paths: 13 | /pets: 14 | get: 15 | summary: List all pets 16 | responses: 17 | '200': 18 | description: A paged array of pets 19 | content: 20 | application/json: 21 | schema: 22 | $ref: "components.yaml#/components/schemas/Pets" 23 | default: 24 | description: Unexpected error 25 | content: 26 | application/json: 27 | schema: 28 | $ref: "components.yaml#/components/schemas/Error" 29 | 30 | '/pets/{petId}': 31 | get: 32 | summary: Info for a specific pet 33 | responses: 34 | '200': 35 | description: Expected response to a valid request 36 | content: 37 | application/json: 38 | schema: 39 | $ref: "components.yaml#/components/schemas/Pet" 40 | default: 41 | description: Unexpected error 42 | content: 43 | application/json: 44 | schema: 45 | $ref: "components.yaml#/components/schemas/Error" 46 | -------------------------------------------------------------------------------- /tests/fixtures/relative_refs/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /swagger 8 | 9 | paths: 10 | /pets: 11 | get: 12 | summary: List all pets 13 | responses: 14 | '200': 15 | description: A paged array of pets 16 | schema: 17 | type: array 18 | items: 19 | $ref: 'definitions.yaml#/definitions/Pets' 20 | default: 21 | description: Unexpected Error 22 | schema: 23 | $ref: 'definitions.yaml#/definitions/Error' 24 | 25 | '/pets/{id}': 26 | get: 27 | summary: Info for a specific pet 28 | responses: 29 | '200': 30 | description: Expected response to a valid request 31 | schema: 32 | $ref: 'definitions.yaml#/definitions/Pet' 33 | default: 34 | description: Unexpected Error 35 | schema: 36 | $ref: 'definitions.yaml#/definitions/Error' 37 | -------------------------------------------------------------------------------- /tests/fixtures/secure_api/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | servers: 3 | - url: /v1.0 4 | info: 5 | title: '{{title}}' 6 | version: '1.0' 7 | security: 8 | - oauth: 9 | - myscope 10 | paths: 11 | '/greeting/{name}': 12 | post: 13 | summary: Generate greeting 14 | description: Generates a greeting message. 15 | operationId: fakeapi.hello.post_greeting 16 | responses: 17 | '200': 18 | description: greeting response 19 | content: 20 | '*/*': 21 | schema: 22 | type: object 23 | parameters: 24 | - name: name 25 | in: path 26 | description: Name of the person to greet. 27 | required: true 28 | schema: 29 | type: string 30 | '/greeting_basic': 31 | post: 32 | summary: Generate greeting 33 | description: Generates a greeting message. 34 | operationId: fakeapi.hello.post_greeting_basic 35 | responses: 36 | '200': 37 | description: greeting response 38 | content: 39 | '*/*': 40 | schema: 41 | type: object 42 | security: 43 | - basic: [] 44 | '/greeting_oidc': 45 | post: 46 | summary: Generate greeting 47 | description: Generates a greeting message. 48 | operationId: fakeapi.hello.post_greeting_oidc 49 | responses: 50 | '200': 51 | description: greeting response 52 | content: 53 | '*/*': 54 | schema: 55 | type: object 56 | security: 57 | - openIdConnect: 58 | - mytestscope 59 | components: 60 | securitySchemes: 61 | oauth: 62 | type: oauth2 63 | x-tokenInfoUrl: 'https://oauth.example/token_info' 64 | flows: 65 | password: 66 | tokenUrl: 'https://oauth.example/token' 67 | scopes: 68 | myscope: can do stuff 69 | basic: 70 | type: http 71 | scheme: basic 72 | description: Basic auth 73 | x-basicInfoFunc: fakeapi.auth.fake_basic_auth 74 | 75 | openIdConnect: 76 | type: openIdConnect 77 | description: Fake OIDC auth 78 | openIdConnectUrl: https://oauth.example/.well-known/openid-configuration 79 | -------------------------------------------------------------------------------- /tests/fixtures/secure_api/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | securityDefinitions: 10 | oauth: 11 | type: oauth2 12 | flow: password 13 | tokenUrl: https://oauth.example/token 14 | x-tokenInfoUrl: https://oauth.example/token_info 15 | scopes: 16 | myscope: can do stuff 17 | basic: 18 | type: basic 19 | description: Basic auth 20 | x-basicInfoFunc: fakeapi.auth.fake_basic_auth 21 | 22 | security: 23 | - oauth: 24 | - myscope 25 | 26 | paths: 27 | /greeting/{name}: 28 | post: 29 | summary: Generate greeting 30 | description: Generates a greeting message. 31 | operationId: fakeapi.hello.post_greeting 32 | responses: 33 | '200': 34 | description: greeting response 35 | schema: 36 | type: object 37 | parameters: 38 | - name: name 39 | in: path 40 | description: Name of the person to greet. 41 | required: true 42 | type: string 43 | format: path 44 | 45 | /greeting_basic/: 46 | post: 47 | summary: Generate greeting 48 | description: Generates a greeting message. 49 | operationId: fakeapi.hello.post_greeting_basic 50 | responses: 51 | '200': 52 | description: greeting response 53 | schema: 54 | type: object 55 | security: 56 | - basic: [] 57 | -------------------------------------------------------------------------------- /tests/fixtures/simple/basepath-slash.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "Test basePath == /" 5 | version: "1.0" 6 | 7 | basePath: / 8 | 9 | paths: 10 | /greeting/{name}: 11 | post: 12 | summary: Generate greeting 13 | description: Generates a greeting message. 14 | operationId: fakeapi.hello.post_greeting 15 | responses: 16 | '200': 17 | description: greeting response 18 | schema: 19 | type: object 20 | parameters: 21 | - name: name 22 | in: path 23 | description: Name of the person to greet. 24 | required: true 25 | type: string 26 | -------------------------------------------------------------------------------- /tests/fixtures/unordered_definition/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | '/unordered-params/{path_param}': 7 | get: 8 | summary: Mixed parameters in swagger definition 9 | operationId: fakeapi.hello.unordered_params_response 10 | responses: 11 | '200': 12 | description: OK 13 | parameters: 14 | - name: first 15 | in: query 16 | description: First Param 17 | schema: 18 | type: integer 19 | - name: path_param 20 | in: path 21 | required: true 22 | description: Path Param 23 | schema: 24 | type: string 25 | - name: second 26 | in: query 27 | description: Second Param 28 | schema: 29 | type: integer 30 | servers: 31 | - url: /v1.0 32 | -------------------------------------------------------------------------------- /tests/fixtures/unordered_definition/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.0 8 | 9 | paths: 10 | /unordered-params/{path_param}: 11 | get: 12 | summary: Mixed parameters in swagger definition 13 | operationId: fakeapi.hello.unordered_params_response 14 | responses: 15 | '200': 16 | description: OK 17 | parameters: 18 | - name: first 19 | in: query 20 | type: integer 21 | description: First Param 22 | - name: path_param 23 | in: path 24 | required: true 25 | type: string 26 | description: Path Param 27 | - name: second 28 | in: query 29 | type: integer 30 | description: Second Param 31 | -------------------------------------------------------------------------------- /tests/fixtures/user_module_loading_error/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: '{{title}}' 4 | version: '1.0' 5 | paths: 6 | /welcome: 7 | get: 8 | operationId: fakeapi.module_with_exception.something 9 | responses: 10 | '200': 11 | description: greeting response 12 | content: 13 | '*/*': 14 | schema: 15 | type: object 16 | servers: 17 | - url: /v1.1 18 | -------------------------------------------------------------------------------- /tests/fixtures/user_module_loading_error/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | 3 | info: 4 | title: "{{title}}" 5 | version: "1.0" 6 | 7 | basePath: /v1.1 8 | 9 | paths: 10 | /welcome: 11 | get: 12 | operationId: fakeapi.module_with_exception.something 13 | responses: 14 | '200': 15 | description: greeting response 16 | schema: 17 | type: object 18 | -------------------------------------------------------------------------------- /tests/test_datastructures.py: -------------------------------------------------------------------------------- 1 | from connexion.datastructures import MediaTypeDict 2 | 3 | 4 | def test_media_type_dict(): 5 | d = MediaTypeDict( 6 | { 7 | "*/*": "*/*", 8 | "*/json": "*/json", 9 | "*/*json": "*/*json", 10 | "multipart/*": "multipart/*", 11 | "multipart/form-data": "multipart/form-data", 12 | } 13 | ) 14 | 15 | assert d["application/json"] == "*/json" 16 | assert d["application/problem+json"] == "*/*json" 17 | assert d["application/x-www-form-urlencoded"] == "*/*" 18 | assert d["multipart/form-data"] == "multipart/form-data" 19 | assert d["multipart/byteranges"] == "multipart/*" 20 | 21 | # Test __contains__ 22 | assert "application/json" in d 23 | -------------------------------------------------------------------------------- /tests/test_flask_encoder.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import math 4 | from decimal import Decimal 5 | 6 | from connexion.frameworks.flask import FlaskJSONProvider 7 | 8 | from conftest import build_app_from_fixture 9 | 10 | 11 | def test_json_encoder(): 12 | json_encoder = json.JSONEncoder 13 | json_encoder.default = FlaskJSONProvider.default 14 | 15 | s = json.dumps({1: 2}, cls=json_encoder) 16 | assert '{"1": 2}' == s 17 | 18 | s = json.dumps(datetime.date.today(), cls=json_encoder) 19 | assert len(s) == 12 20 | 21 | s = json.dumps(datetime.datetime.utcnow(), cls=json_encoder) 22 | assert s.endswith('Z"') 23 | 24 | s = json.dumps(Decimal(1.01), cls=json_encoder) 25 | assert s == "1.01" 26 | 27 | s = json.dumps(math.expm1(1e-10), cls=json_encoder) 28 | assert s == "1.00000000005e-10" 29 | 30 | 31 | def test_json_encoder_datetime_with_timezone(): 32 | json_encoder = json.JSONEncoder 33 | json_encoder.default = FlaskJSONProvider.default 34 | 35 | class DummyTimezone(datetime.tzinfo): 36 | def utcoffset(self, dt): 37 | return datetime.timedelta(0) 38 | 39 | def dst(self, dt): 40 | return datetime.timedelta(0) 41 | 42 | s = json.dumps(datetime.datetime.now(DummyTimezone()), cls=json_encoder) 43 | assert s.endswith('+00:00"') 44 | 45 | 46 | def test_readonly(json_datetime_dir, spec, app_class): 47 | app = build_app_from_fixture( 48 | json_datetime_dir, app_class=app_class, spec_file=spec, validate_responses=True 49 | ) 50 | app_client = app.test_client() 51 | 52 | res = app_client.get("/v1.0/" + spec.replace("yaml", "json")) 53 | assert res.status_code == 200, f"Error is {res.text}" 54 | spec_data = res.json() 55 | 56 | if spec == "openapi.yaml": 57 | response_path = "responses.200.content.application/json.schema" 58 | else: 59 | response_path = "responses.200.schema" 60 | 61 | def get_value(data, path): 62 | for part in path.split("."): 63 | data = data.get(part) 64 | assert data, f"No data in part '{part}' of '{path}'" 65 | return data 66 | 67 | example = get_value(spec_data, f"paths./datetime.get.{response_path}.example.value") 68 | assert example in [ 69 | "2000-01-23T04:56:07.000008+00:00", # PyYAML 5.3+ 70 | "2000-01-23T04:56:07.000008Z", 71 | ] 72 | example = get_value(spec_data, f"paths./date.get.{response_path}.example.value") 73 | assert example == "2000-01-23" 74 | example = get_value(spec_data, f"paths./uuid.get.{response_path}.example.value") 75 | assert example == "a7b8869c-5f24-4ce0-a5d1-3e44c3663aa9" 76 | 77 | res = app_client.get("/v1.0/datetime") 78 | assert res.status_code == 200, f"Error is {res.text}" 79 | data = res.json() 80 | assert data == {"value": "2000-01-02T03:04:05.000006Z"} 81 | 82 | res = app_client.get("/v1.0/date") 83 | assert res.status_code == 200, f"Error is {res.text}" 84 | data = res.json() 85 | assert data == {"value": "2000-01-02"} 86 | 87 | res = app_client.get("/v1.0/uuid") 88 | assert res.status_code == 200, f"Error is {res.text}" 89 | data = res.json() 90 | assert data == {"value": "e7ff66d0-3ec2-4c4e-bed0-6e4723c24c51"} 91 | -------------------------------------------------------------------------------- /tests/test_flask_utils.py: -------------------------------------------------------------------------------- 1 | from connexion.frameworks import flask as flask_utils 2 | 3 | 4 | def test_flaskify_path(): 5 | assert flask_utils.flaskify_path("{test-path}") == "" 6 | assert flask_utils.flaskify_path("api/{test-path}") == "api/" 7 | assert flask_utils.flaskify_path("my-api/{test-path}") == "my-api/" 8 | assert flask_utils.flaskify_path("foo_bar/{a-b}/{c_d}") == "foo_bar//" 9 | assert ( 10 | flask_utils.flaskify_path("foo/{a}/{b}", {"a": "integer"}) == "foo//" 11 | ) 12 | assert ( 13 | flask_utils.flaskify_path("foo/{a}/{b}", {"a": "number"}) == "foo//" 14 | ) 15 | assert flask_utils.flaskify_path("foo/{a}/{b}", {"a": "path"}) == "foo//" 16 | assert flask_utils.flaskify_path("foo/{a}", {"a": "path"}) == "foo/" 17 | 18 | 19 | def test_flaskify_endpoint(): 20 | assert flask_utils.flaskify_endpoint("module.function") == "module_function" 21 | assert flask_utils.flaskify_endpoint("function") == "function" 22 | 23 | name = "module.function" 24 | randlen = 6 25 | res = flask_utils.flaskify_endpoint(name, randlen) 26 | assert res.startswith("module_function") 27 | assert len(res) == len(name) + 1 + randlen 28 | -------------------------------------------------------------------------------- /tests/test_lifespan.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import sys 3 | from unittest import mock 4 | 5 | import pytest 6 | from connexion import AsyncApp, ConnexionMiddleware 7 | 8 | 9 | def test_lifespan_handler(app_class): 10 | m = mock.MagicMock() 11 | 12 | @contextlib.asynccontextmanager 13 | async def lifespan(app): 14 | m.startup() 15 | yield 16 | m.shutdown() 17 | 18 | app = AsyncApp(__name__, lifespan=lifespan) 19 | with app.test_client(): 20 | m.startup.assert_called() 21 | m.shutdown.assert_not_called() 22 | m.shutdown.assert_called() 23 | 24 | 25 | @pytest.mark.skipif( 26 | sys.version_info < (3, 8), reason="AsyncMock only available from 3.8." 27 | ) 28 | async def test_lifespan(): 29 | """Test that lifespan events are passed through if no handler is registered.""" 30 | lifecycle_handler = mock.Mock() 31 | 32 | async def check_lifecycle(scope, receive, send): 33 | if scope["type"] == "lifespan": 34 | lifecycle_handler.handle() 35 | 36 | test_app = ConnexionMiddleware(check_lifecycle) 37 | await test_app({"type": "lifespan"}, mock.AsyncMock(), mock.AsyncMock()) 38 | lifecycle_handler.handle.assert_called() 39 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | from connexion import FlaskApp 6 | from connexion.middleware import ConnexionMiddleware, MiddlewarePosition 7 | from connexion.middleware.swagger_ui import SwaggerUIMiddleware 8 | from connexion.types import Environ, ResponseStream, StartResponse, WSGIApp 9 | from starlette.datastructures import MutableHeaders 10 | 11 | from conftest import build_app_from_fixture 12 | 13 | 14 | class TestMiddleware: 15 | """Middleware to check if operation is accessible on scope.""" 16 | 17 | __test__ = False 18 | 19 | def __init__(self, app): 20 | self.app = app 21 | 22 | async def __call__(self, scope, receive, send): 23 | operation_id = scope["extensions"]["connexion_routing"]["operation_id"] 24 | 25 | async def patched_send(message): 26 | if message["type"] != "http.response.start": 27 | await send(message) 28 | return 29 | 30 | message.setdefault("headers", []) 31 | headers = MutableHeaders(scope=message) 32 | headers["operation_id"] = operation_id 33 | 34 | await send(message) 35 | 36 | await self.app(scope, receive, patched_send) 37 | 38 | 39 | @pytest.fixture(scope="session") 40 | def middleware_app(spec, app_class): 41 | middlewares = ConnexionMiddleware.default_middlewares + [TestMiddleware] 42 | return build_app_from_fixture( 43 | "simple", app_class=app_class, spec_file=spec, middlewares=middlewares 44 | ) 45 | 46 | 47 | def test_routing_middleware(middleware_app): 48 | app_client = middleware_app.test_client() 49 | 50 | response = app_client.post("/v1.0/greeting/robbe") 51 | 52 | assert ( 53 | response.headers.get("operation_id") == "fakeapi.hello.post_greeting" 54 | ), response.status_code 55 | 56 | 57 | def test_add_middleware(spec, app_class): 58 | """Test adding middleware via the `add_middleware` method.""" 59 | app = build_app_from_fixture("simple", app_class=app_class, spec_file=spec) 60 | app.add_middleware(TestMiddleware) 61 | 62 | app_client = app.test_client() 63 | response = app_client.post("/v1.0/greeting/robbe") 64 | 65 | assert ( 66 | response.headers.get("operation_id") == "fakeapi.hello.post_greeting" 67 | ), response.status_code 68 | 69 | 70 | def test_position(spec, app_class): 71 | """Test adding middleware via the `add_middleware` method.""" 72 | middlewares = [ 73 | middleware 74 | for middleware in ConnexionMiddleware.default_middlewares 75 | if middleware != SwaggerUIMiddleware 76 | ] 77 | app = build_app_from_fixture( 78 | "simple", app_class=app_class, spec_file=spec, middlewares=middlewares 79 | ) 80 | 81 | with pytest.raises(ValueError) as exc_info: 82 | app.add_middleware(TestMiddleware, position=MiddlewarePosition.BEFORE_SWAGGER) 83 | 84 | assert ( 85 | exc_info.value.args[0] 86 | == f"Could not insert middleware at position BEFORE_SWAGGER. " 87 | f"Please make sure you have a {SwaggerUIMiddleware} in your stack." 88 | ) 89 | 90 | 91 | def test_add_wsgi_middleware(spec): 92 | app: FlaskApp = build_app_from_fixture("simple", app_class=FlaskApp, spec_file=spec) 93 | 94 | class WSGIMiddleware: 95 | def __init__(self, app_: WSGIApp, mock_counter): 96 | self.next_app = app_ 97 | self.mock_counter = mock_counter 98 | 99 | def __call__( 100 | self, environ: Environ, start_response: StartResponse 101 | ) -> ResponseStream: 102 | self.mock_counter() 103 | return self.next_app(environ, start_response) 104 | 105 | mock = Mock() 106 | app.add_wsgi_middleware(WSGIMiddleware, mock_counter=mock) 107 | 108 | app_client = app.test_client() 109 | app_client.post("/v1.0/greeting/robbe") 110 | 111 | mock.assert_called_once() 112 | -------------------------------------------------------------------------------- /tests/test_references.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | from connexion.json_schema import RefResolutionError, resolve_refs 5 | from connexion.jsonifier import Jsonifier 6 | 7 | DEFINITIONS = { 8 | "new_stack": { 9 | "required": ["image_version", "keep_stacks", "new_traffic", "senza_yaml"], 10 | "type": "object", 11 | "properties": { 12 | "keep_stacks": { 13 | "type": "integer", 14 | "description": "Number of older stacks to keep", 15 | }, 16 | "image_version": { 17 | "type": "string", 18 | "description": "Docker image version to deploy", 19 | }, 20 | "senza_yaml": {"type": "string", "description": "YAML to provide to senza"}, 21 | "new_traffic": { 22 | "type": "integer", 23 | "description": "Percentage of the traffic", 24 | }, 25 | }, 26 | }, 27 | "composed": { 28 | "required": ["test"], 29 | "type": "object", 30 | "properties": {"test": {"schema": {"$ref": "#/definitions/new_stack"}}}, 31 | }, 32 | "problem": {"some": "thing"}, 33 | } 34 | PARAMETER_DEFINITIONS = {"myparam": {"in": "path", "type": "integer"}} 35 | 36 | 37 | @pytest.fixture 38 | def api(): 39 | return mock.MagicMock(jsonifier=Jsonifier) 40 | 41 | 42 | def test_non_existent_reference(api): 43 | op_spec = { 44 | "parameters": [ 45 | { 46 | "in": "body", 47 | "name": "new_stack", 48 | "required": True, 49 | "schema": {"$ref": "#/definitions/new_stack"}, 50 | } 51 | ] 52 | } 53 | with pytest.raises(RefResolutionError) as exc_info: # type: py.code.ExceptionInfo 54 | resolve_refs(op_spec, {}) 55 | 56 | exception = exc_info.value 57 | assert "definitions/new_stack" in str(exception) 58 | 59 | 60 | def test_invalid_reference(api): 61 | op_spec = { 62 | "parameters": [ 63 | { 64 | "in": "body", 65 | "name": "new_stack", 66 | "required": True, 67 | "schema": {"$ref": "#/notdefinitions/new_stack"}, 68 | } 69 | ] 70 | } 71 | 72 | with pytest.raises(RefResolutionError) as exc_info: # type: py.code.ExceptionInfo 73 | resolve_refs( 74 | op_spec, {"definitions": DEFINITIONS, "parameters": PARAMETER_DEFINITIONS} 75 | ) 76 | 77 | exception = exc_info.value 78 | assert "notdefinitions/new_stack" in str(exception) 79 | 80 | 81 | def test_resolve_invalid_reference(api): 82 | op_spec = { 83 | "operationId": "fakeapi.hello.post_greeting", 84 | "parameters": [{"$ref": "/parameters/fail"}], 85 | } 86 | 87 | with pytest.raises(RefResolutionError) as exc_info: 88 | resolve_refs(op_spec, {"parameters": PARAMETER_DEFINITIONS}) 89 | 90 | exception = exc_info.value 91 | assert "parameters/fail" in str(exception) 92 | 93 | 94 | def test_resolve_web_reference(api): 95 | op_spec = {"parameters": [{"$ref": "https://reallyfake.asd/parameters.json"}]} 96 | store = {"https://reallyfake.asd/parameters.json": {"name": "test"}} 97 | 98 | spec = resolve_refs(op_spec, store=store) 99 | assert spec["parameters"][0]["name"] == "test" 100 | 101 | 102 | def test_resolve_ref_referring_to_another_ref(api): 103 | expected = {"type": "string"} 104 | op_spec = { 105 | "parameters": [ 106 | { 107 | "schema": {"$ref": "#/definitions/A"}, 108 | } 109 | ], 110 | "definitions": { 111 | "A": { 112 | "$ref": "#/definitions/B", 113 | }, 114 | "B": expected, 115 | }, 116 | } 117 | 118 | spec = resolve_refs(op_spec) 119 | assert spec["parameters"][0]["schema"] == expected 120 | assert spec["definitions"]["A"] == expected 121 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude=connexion/__init__.py 3 | rst-roles=class,mod,obj 4 | # https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 5 | # Longest docstring in current code base 6 | max-line-length=137 7 | extend-ignore=E203,RST303 8 | 9 | [tox] 10 | isolated_build = True 11 | envlist = 12 | py38-min 13 | {py38,py39,py310,py311,py312}-pypi 14 | pre-commit 15 | 16 | [gh-actions] 17 | python = 18 | 3.8: py38-min,py38-pypi 19 | 3.9: py39-pypi 20 | 3.10: py310-pypi 21 | 3.11: py311-pypi,pre-commit 22 | 3.12: py312-pypi 23 | 24 | [testenv] 25 | setenv=PYTHONPATH = {toxinidir}:{toxinidir} 26 | deps= 27 | poetry 28 | allowlist_externals= 29 | sed 30 | mv 31 | commands= 32 | # sed in-place flag works on Linux and Mac, writes a .bak file 33 | min: sed -i.bak -E 's/"(\^|~|>=)([ 0-9])/"==\2/' pyproject.toml 34 | poetry lock 35 | poetry install --all-extras --with tests 36 | poetry show 37 | poetry run python -m pytest tests --cov connexion --cov-report term-missing 38 | min: mv -f pyproject.toml.bak pyproject.toml 39 | 40 | [testenv:pre-commit] 41 | deps=pre-commit 42 | commands=pre-commit run --all-files --show-diff-on-failure 43 | --------------------------------------------------------------------------------