├── tests
├── __init__.py
├── acceptance
│ ├── __init__.py
│ ├── app
│ │ ├── config.ini
│ │ └── __init__.py
│ ├── invalid_file_test.py
│ ├── recursive_app_test.py
│ ├── config_ini_test.py
│ ├── format_test.py
│ ├── prefer_20_routes_test.py
│ ├── yaml_test.py
│ ├── relative_ref_test.py
│ ├── api_test.py
│ ├── response20_test.py
│ ├── request_test.py
│ └── response_test.py
├── sample_schemas
│ ├── external_refs
│ │ ├── A.json
│ │ └── swagger.json
│ ├── bad_app
│ │ ├── api_docs.json
│ │ ├── bad_sample.json
│ │ └── swagger.json
│ ├── prefer_20_routes_app
│ │ ├── api_docs.json
│ │ ├── swagger.json
│ │ └── other_sample.json
│ ├── missing_api_declaration
│ │ └── api_docs.json
│ ├── relative_ref
│ │ ├── parameters
│ │ │ └── common.json
│ │ ├── swagger.json
│ │ ├── responses
│ │ │ └── common.json
│ │ ├── paths
│ │ │ └── common.json
│ │ └── dereferenced_swagger.json
│ ├── recursive_app
│ │ ├── external
│ │ │ ├── external.json
│ │ │ └── swagger.json
│ │ └── internal
│ │ │ └── swagger.json
│ ├── good_app
│ │ ├── post_endpoint_with_optional_body.json
│ │ ├── api_docs.json
│ │ ├── no_models.json
│ │ ├── echo_date.json
│ │ ├── other_sample.json
│ │ ├── sample.json
│ │ └── swagger.json
│ ├── missing_resource_listing
│ │ └── bad_sample.json
│ ├── nested_defns
│ │ └── swagger.yaml
│ ├── yaml_app
│ │ ├── swagger.yaml
│ │ └── defs.yaml
│ └── user_format
│ │ └── swagger.json
├── conftest.py
├── spec_test.py
├── load_schema_test.py
├── model_test.py
├── includeme_test.py
├── api_test.py
└── ingest_test.py
├── .deactivate.sh
├── .activate.sh
├── .landscape.yaml
├── setup.cfg
├── requirements-dev.txt
├── MANIFEST.in
├── .gitignore
├── .github
└── workflows
│ ├── ci.yaml
│ └── pypi.yaml
├── Makefile
├── docs
├── external_resources.rst
├── glossary.rst
├── what_is_swagger.rst
├── index.rst
├── migrating_to_swagger_20.rst
├── conf.py
└── changelog.rst
├── .appveyor.yml
├── pyramid_swagger
├── __about__.py
├── spec.py
├── exceptions.py
├── renderer.py
├── __init__.py
├── model.py
├── ingest.py
├── load_schema.py
└── api.py
├── tox.ini
├── .pre-commit-config.yaml
├── setup.py
├── LICENSE
└── README.rst
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/acceptance/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.deactivate.sh:
--------------------------------------------------------------------------------
1 | deactivate
2 |
--------------------------------------------------------------------------------
/.activate.sh:
--------------------------------------------------------------------------------
1 | source venv/bin/activate
2 |
--------------------------------------------------------------------------------
/.landscape.yaml:
--------------------------------------------------------------------------------
1 | strictness: high
2 | ignore-paths:
3 | - docs
4 | - setup.py
5 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.rst
3 |
4 | [wheel]
5 | universal = True
6 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | -e .
2 | bravado
3 | coverage
4 | mock
5 | ordereddict
6 | pre-commit
7 | pytest>=3
8 | tox
9 | webtest
10 |
--------------------------------------------------------------------------------
/tests/sample_schemas/external_refs/A.json:
--------------------------------------------------------------------------------
1 | {
2 | "properties": {
3 | "A": {
4 | "type": "object"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | # Required by python 2.6 even though we include these in our setup.py
3 | include pyramid_swagger/swagger_spec_schemas/v1.2/*
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache/
2 | .idea/
3 | *pyc
4 | *swp
5 | docs/_build
6 | docs/build
7 | build/
8 | dist/
9 | .coverage
10 | .tox
11 | .ropeproject
12 | pyramid_swagger.egg-info
13 | tests/__pycache__
14 | venv/
15 | .pytest_cache/
16 |
--------------------------------------------------------------------------------
/tests/sample_schemas/bad_app/api_docs.json:
--------------------------------------------------------------------------------
1 | {
2 | "swaggerVersion": "1.2",
3 | "apis": [
4 | {
5 | "path": "/bad_sample",
6 | "description": "An invalid api declaration."
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/tests/sample_schemas/prefer_20_routes_app/api_docs.json:
--------------------------------------------------------------------------------
1 | {
2 | "swaggerVersion": "1.2",
3 | "apis": [
4 | {
5 | "path": "/other_sample",
6 | "description": "Two heads are better than one."
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/tests/sample_schemas/missing_api_declaration/api_docs.json:
--------------------------------------------------------------------------------
1 | {
2 | "swaggerVersion": "1.2",
3 | "apis": [
4 | {
5 | "path": "/missing",
6 | "description": "An api declaration that does not exist."
7 | }
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/tests/sample_schemas/relative_ref/parameters/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "path_arg": {
3 | "in": "path",
4 | "name": "path_arg",
5 | "required": true,
6 | "type": "string",
7 | "enum": ["path_arg1", "path_arg2"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/sample_schemas/external_refs/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "version": "1.0.0",
5 | "title": "dumb spec"
6 | },
7 | "paths": {
8 | "/blah": {
9 | "get": {
10 | "responses": {
11 | "200": {
12 | "description": "blah",
13 | "schema": {
14 | "$ref": "A.json"
15 | }
16 | }
17 | }
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: build
3 | on: push
4 | jobs:
5 | tox:
6 | runs-on: ubuntu-22.04
7 | strategy:
8 | fail-fast: false
9 | matrix:
10 | tox:
11 | - '3.8'
12 | - '3.10'
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: actions/setup-python@v4
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 | - run: pip install tox
19 | - run: tox -e py
20 |
--------------------------------------------------------------------------------
/tests/sample_schemas/recursive_app/external/external.json:
--------------------------------------------------------------------------------
1 | {
2 | "widget": {
3 | "description": "A widget which may have multiple generations of child widgets",
4 | "properties": {
5 | "name": {
6 | "type": "string",
7 | "minLength": 1,
8 | "maxLength": 50
9 | },
10 | "children": {
11 | "type": "array",
12 | "items": {
13 | "$ref":"#/widget"
14 | }
15 | }
16 | },
17 | "type": "object"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean docs install-hooks test
2 |
3 | all: venv install-hooks
4 |
5 | test: install-hooks
6 | tox
7 |
8 | install-hooks: venv
9 | venv/bin/pre-commit install -f --install-hooks
10 |
11 | venv: setup.py requirements-dev.txt
12 | virtualenv venv
13 | venv/bin/pip install -r requirements-dev.txt
14 |
15 | docs:
16 | tox -e docs
17 | mkdir -p docs/build
18 | cp -r docs/_build/html docs/build/html
19 |
20 | clean:
21 | find . -type f -iname "*.py[co]" -delete
22 | rm -fr *.egg-info/
23 | rm -fr .tox/
24 | rm -fr venv/
25 |
--------------------------------------------------------------------------------
/docs/external_resources.rst:
--------------------------------------------------------------------------------
1 | External resources
2 | ===========================================
3 |
4 | There are a variety of external resources you will find useful when documenting
5 | your API with Swagger.
6 |
7 | * `Interactive Spec Editor `_
8 | * `Swagger 1.2 Specification `_
9 | * `Swagger 2.0 Specification `_
10 | * `"Pet Store" example API `_
11 |
--------------------------------------------------------------------------------
/tests/sample_schemas/relative_ref/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "Title was not specified",
5 | "version": "0.1"
6 | },
7 | "produces": ["application/json"],
8 | "paths": {
9 | "/no_models": {
10 | "$ref": "paths/common.json#/no_models"
11 | },
12 | "/sample/{path_arg}/resource": {
13 | "$ref": "paths/common.json#/sample_resource"
14 | }
15 | },
16 | "host": "localhost:9999",
17 | "schemes": [
18 | "http"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/tests/sample_schemas/relative_ref/responses/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "200": {
3 | "description": "Return a standard_response",
4 | "schema": {
5 | "type": "object",
6 | "required": [
7 | "raw_response",
8 | "logging_info"
9 | ],
10 | "additionalProperties": false,
11 | "properties": {
12 | "raw_response": {
13 | "type": "string"
14 | },
15 | "logging_info": {
16 | "type": "object"
17 | }
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/acceptance/app/config.ini:
--------------------------------------------------------------------------------
1 | [app:main]
2 | use = call:tests.acceptance.app:main
3 | pyramid_swagger.enable_request_validation = true
4 | pyramid_swagger.enable_response_validation = false
5 | pyramid_swagger.enable_path_validation = true
6 | pyramid_swagger.exclude_routes = /undefined/first
7 | /undefined/second
8 | pyramid_swagger.prefer_20_routes = /sample
9 | pyramid_swagger.schema_directory = tests/sample_schemas/good_app
10 | pyramid_swagger.swagger_versions = 1.2 2.0
11 | pyramid_swagger.response_validation_exclude_routes = /exclude_response/first
12 | /exclude_response/second
13 |
--------------------------------------------------------------------------------
/.appveyor.yml:
--------------------------------------------------------------------------------
1 | version: '{build}'
2 | image: Visual Studio 2017
3 |
4 | environment:
5 | matrix:
6 | # Available python versions and their locations on https://www.appveyor.com/docs/build-environment/#python
7 | - PYTHON: C:\Python27-x64
8 | TOXENV: py27
9 | - PYTHON: C:\Python27-x64
10 | TOXENV: py27-pyramid15
11 | - PYTHON: C:\Python35-x64
12 | TOXENV: py35
13 | - PYTHON: C:\Python36-x64
14 | TOXENV: py36
15 |
16 | build: off
17 |
18 | install:
19 | - cmd: SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%
20 | - cmd: pip install tox
21 |
22 | before_test:
23 | - cmd: python --version
24 | - cmd: pip --version
25 | - cmd: tox --version
26 |
27 | test_script:
28 | - cmd: tox
29 |
--------------------------------------------------------------------------------
/pyramid_swagger/__about__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 | from __future__ import division
4 | from __future__ import print_function
5 |
6 |
7 | __all__ = [
8 | "__title__", "__summary__", "__uri__", "__version__", "__author__",
9 | "__email__", "__license__", "__copyright__",
10 | ]
11 |
12 | __title__ = "pyramid_swagger"
13 | __summary__ = "Swagger tools for use in pyramid webapps"
14 | __uri__ = "https://github.com/striglia/pyramid_swagger"
15 |
16 | __version__ = "2.9.0"
17 |
18 | __author__ = "Scott Triglia"
19 | __email__ = "scott.triglia@gmail.com"
20 |
21 | __license__ = "BSD 3-clause"
22 | __copyright__ = "Copyright 2014 Scott Triglia"
23 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import os
5 |
6 | import pytest
7 |
8 |
9 | @pytest.yield_fixture(autouse=True)
10 | def _set_current_directory_to_git_repo_root():
11 | """
12 | During tests execution make sure that current working directory
13 | is set accordingly to git repository root.
14 | It will allow to run tests with relative file import with the
15 | same reference path.
16 | """
17 | repo_root_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
18 | curdir = os.getcwd()
19 | try:
20 | os.chdir(repo_root_path)
21 | yield
22 | finally:
23 | os.chdir(curdir)
24 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py38,py310
3 |
4 | [testenv]
5 | skip_install = True
6 | deps = -rrequirements-dev.txt
7 | pyramid15: pyramid<=1.5.4
8 |
9 | commands =
10 | coverage run --source=pyramid_swagger/ --omit=pyramid_swagger/__about__.py -m pytest --capture=no --strict {posargs:tests/}
11 | coverage report -m
12 | pre-commit run --all-files
13 |
14 | [flake8]
15 | exclude = .svn,CVS,.bzr,.hg,.git,__pycache__,.tox,*.egg,docs/conf.py
16 | max_line_length = 120
17 |
18 | [testenv:docs]
19 | deps =
20 | sphinx
21 | sphinx-rtd-theme
22 | commands =
23 | sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html
24 | sphinx-build -W -b linkcheck docs docs/_build/html
25 |
--------------------------------------------------------------------------------
/tests/sample_schemas/good_app/post_endpoint_with_optional_body.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "0.1",
3 | "swaggerVersion": "1.2",
4 | "basePath": "http://localhost:9999/post_endpoint_with_optional_body",
5 | "apis": [
6 | {
7 | "path": "/post_endpoint_with_optional_body",
8 | "operations": [
9 | {
10 | "method": "POST",
11 | "nickname": "post_endpoint_with_optional_body",
12 | "type": "integer",
13 | "parameters": [
14 | {
15 | "name": "body",
16 | "paramType": "body",
17 | "required": false,
18 | "type": "object"
19 | }
20 | ]
21 | }
22 | ]
23 | }
24 | ],
25 | "models": {
26 | "object": {
27 | "id": "object",
28 | "properties": {
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/sample_schemas/missing_resource_listing/bad_sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "0.1",
3 | "swaggerVersion": "1.2",
4 | "basePath": "http://localhost:9999/sample",
5 | "apis": [
6 | {
7 | "path": "/required_body",
8 | "description": "requires a body",
9 | "operations": [
10 | {
11 | "method": "GET",
12 | "type": "object",
13 | "parameters": [
14 | {
15 | "paramType": "query",
16 | "name": "content",
17 | "required": true
18 | }
19 | ],
20 | "summary": "Tests required body parameters"
21 | }
22 | ]
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/tests/sample_schemas/relative_ref/paths/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "no_models": {
3 | "get": {
4 | "responses": {
5 | "200": {
6 | "$ref": "../responses/common.json#/200"
7 | }
8 | },
9 | "description": "",
10 | "operationId": "no_models_get"
11 | }
12 | },
13 | "sample_resource": {
14 | "get": {
15 | "responses": {
16 | "200": {
17 | "$ref": "../responses/common.json#/200"
18 | }
19 | },
20 | "description": "",
21 | "operationId": "standard",
22 | "parameters": [
23 | {
24 | "$ref": "../parameters/common.json#/path_arg"
25 | }
26 | ]
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yaml:
--------------------------------------------------------------------------------
1 | name: pypi
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | jobs:
9 | test:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-python@v4
14 | with:
15 | python-version: '3.10'
16 | - run: python -m pip install --upgrade setuptools pip 'tox<4' virtualenv
17 | - run: tox -e py310
18 | pypi:
19 | needs: test
20 | runs-on: ubuntu-latest
21 | environment:
22 | name: pypi
23 | url: https://pypi.org/p/pyramid-swagger
24 | permissions:
25 | id-token: write
26 | steps:
27 | - uses: actions/checkout@v3
28 | - uses: actions/setup-python@v4
29 | with:
30 | python-version: '3.10'
31 | - run: python setup.py sdist
32 | - uses: pypa/gh-action-pypi-publish@v1.8.10
33 |
--------------------------------------------------------------------------------
/tests/sample_schemas/bad_app/bad_sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "0.1",
3 | "swaggerVersion": "1.2",
4 | "basePath": "http://localhost:9999/sample",
5 | "apis": [
6 | {
7 | "path": "/required_body",
8 | "description": "requires a body",
9 | "operations": [
10 | {
11 | "method": "GET",
12 | "type": "object",
13 | "parameters": [
14 | {
15 | "paramType": "query",
16 | "name": "content",
17 | "required": true,
18 | "type": "string"
19 | }
20 | ],
21 | "summary": "Tests required body parameters"
22 | }
23 | ]
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/tests/sample_schemas/good_app/api_docs.json:
--------------------------------------------------------------------------------
1 | {
2 | "swaggerVersion": "1.2",
3 | "apis": [
4 | {
5 | "path": "/no_models",
6 | "description": "As it says."
7 | },
8 | {
9 | "path": "/sample",
10 | "description": "Sample valid api declaration."
11 | },
12 | {
13 | "path": "/other_sample",
14 | "description": "Two heads are better than one."
15 | },
16 | {
17 | "path": "/echo_date",
18 | "description": "Echoes the input body in the response. Endpoint used for verifying PyramidSwaggerRendererFactory"
19 | },
20 | {
21 | "path": "/post_endpoint_with_optional_body",
22 | "description": "Returns the request body length. Used to verify that optional body parameters are well handled"
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/tests/sample_schemas/good_app/no_models.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "0.1",
3 | "swaggerVersion": "1.2",
4 | "basePath": "http://localhost:9999/no_models",
5 | "apis": [
6 | {
7 | "path": "/no_models",
8 | "operations": [
9 | {
10 | "method": "GET",
11 | "nickname": "no_models_get",
12 | "type": "string",
13 | "parameters": []
14 | }
15 | ]
16 | },
17 | {
18 | "path": "/throw_400",
19 | "operations": [
20 | {
21 | "method": "GET",
22 | "nickname": "throw_400",
23 | "type": "string",
24 | "parameters": []
25 | }
26 | ]
27 | }
28 | ],
29 | "models": {
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/docs/glossary.rst:
--------------------------------------------------------------------------------
1 | Glossary of Terms
2 | ===========================================
3 |
4 | Nothing more than some common vocabulary for you to absorb.
5 |
6 | swagger api-docs (often swagger api)
7 | The preferred term for the resource listing and associated api declarations. This is so-named to avoid confusion with the Swagger Specification and the actual implementation of your service.
8 |
9 | resource listing
10 | The top-level declaration of the various Swagger resources your service exposes. Each resource must have an associated api declaration.
11 |
12 | api declaration
13 | The description of each endpoint a particular Swagger service provides, with complete input and output declared.
14 |
15 | swagger spec
16 | The formal specification of a valid swagger api. The current public version is 2.0 and hosted on `wordnik's Github `_.
17 |
--------------------------------------------------------------------------------
/docs/what_is_swagger.rst:
--------------------------------------------------------------------------------
1 | What is Swagger?
2 | =================
3 |
4 | Basic working knowledge of Swagger is a prerequisite for having pyramid_swagger
5 | make sense as a library.
6 |
7 | `Swagger ` is a specification format for describing HTTP services, with a particular focus on RESTful APIs. The schema you write will describe your API comprehensively.
8 |
9 | The benefit of going through the work of writing a Swagger schema for API is you then get access to a great number of tools which work off this spec. The Swagger website has an entire page devoted to `community tools which consume this schema `. In fact, you'll notice that pyramid_swagger is listed as one of these.
10 |
11 | In short, your Swagger schema simply describes your API. For a more in-depth introduction, try `the official Swagger introduction article`.
12 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v2.2.1
4 | hooks:
5 | - id: check-json
6 | - id: check-yaml
7 | - id: debug-statements
8 | - id: end-of-file-fixer
9 | - id: fix-encoding-pragma
10 | - id: name-tests-test
11 | - id: trailing-whitespace
12 | - repo: https://github.com/pre-commit/mirrors-autopep8
13 | rev: v1.4.4
14 | hooks:
15 | - id: autopep8
16 | args:
17 | - -i
18 | - --ignore=E309,E501
19 | - repo: https://github.com/PyCQA/flake8
20 | rev: 3.7.6
21 | hooks:
22 | - id: flake8
23 | args:
24 | - --ignore=W503
25 | exclude: ^docs
26 | - repo: https://github.com/asottile/reorder_python_imports
27 | rev: v1.4.0
28 | hooks:
29 | - id: reorder-python-imports
30 | args:
31 | - --add-import
32 | - from __future__ import absolute_import
33 |
--------------------------------------------------------------------------------
/tests/sample_schemas/nested_defns/swagger.yaml:
--------------------------------------------------------------------------------
1 | basePath: /
2 |
3 | definitions:
4 | A:
5 | type: object
6 | B:
7 | allOf:
8 | - properties:
9 | items:
10 | $ref: '#/definitions/A'
11 | C:
12 | properties:
13 | foobar:
14 | $ref: '#/definitions/B'
15 | D:
16 | properties:
17 | foobar:
18 | $ref: '#/definitions/C'
19 | E:
20 | properties:
21 | foobar:
22 | $ref: '#/definitions/D'
23 | F:
24 | properties:
25 | foobar:
26 | $ref: '#/definitions/E'
27 | G:
28 | properties:
29 | foobar:
30 | $ref: '#/definitions/F'
31 | info:
32 | title: A pointless spec
33 | version: 42.42.42
34 |
35 | paths: {}
36 |
37 | produces:
38 | - application/json
39 |
40 | schemes:
41 | - https
42 |
43 | swagger: '2.0'
44 |
--------------------------------------------------------------------------------
/tests/sample_schemas/yaml_app/swagger.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | swagger: "2.0"
3 | info:
4 | title: "Title was not specified"
5 | version: "0.1"
6 | host: "localhost:9999"
7 | schemes:
8 | - "http"
9 | produces:
10 | - "application/json"
11 | paths:
12 | "/sample/{path_arg}/resource":
13 | get:
14 | responses:
15 | "200":
16 | description: "Return a standard_response"
17 | schema:
18 | "$ref": "defs.yaml#/definitions/standard_response"
19 | description: ""
20 | operationId: "standard"
21 | parameters:
22 | - in: "path"
23 | name: "path_arg"
24 | required: true
25 | type: "string"
26 | enum:
27 | - "path_arg1"
28 | - "path_arg2"
29 | - in: "query"
30 | name: "required_arg"
31 | required: true
32 | type: "string"
33 | format: "base64"
34 | - in: "query"
35 | name: "optional_arg"
36 | required: false
37 | type: "string"
38 | post:
39 | $ref: defs.yaml#/operations/post
40 |
--------------------------------------------------------------------------------
/tests/sample_schemas/recursive_app/external/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "Bug demo",
5 | "description": "Recursive object definitions cause problems",
6 | "version": "0.1.0"
7 | },
8 | "definitions": {
9 | "widget": {
10 | "$ref": "external.json#/widget"
11 | }
12 | },
13 | "paths": {
14 | "/resources/widget/{code}" : {
15 | "parameters" : [
16 | {
17 | "name": "code",
18 | "description": "The CODE value for a widget",
19 | "in": "path",
20 | "type": "string",
21 | "required": true
22 | }
23 | ],
24 | "get": {
25 | "summary": "View a single widget",
26 | "produces": ["application/json"],
27 | "parameters": [],
28 | "responses": {
29 | "200": {
30 | "description":"Widget local content (without parents or descendents)",
31 | "schema": {"$ref":"external.json#/widget"}
32 | },
33 | "404": {"description": "The widget does not exist"}
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/spec_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import os
5 |
6 | import pytest
7 | import simplejson
8 | from jsonschema.exceptions import ValidationError
9 |
10 | from pyramid_swagger.spec import API_DOCS_FILENAME
11 | from pyramid_swagger.spec import validate_swagger_schema
12 |
13 |
14 | def test_success_for_good_app():
15 | dir_path = 'tests/sample_schemas/good_app/'.replace('/', os.path.sep)
16 | with open(os.path.join(dir_path, API_DOCS_FILENAME)) as f:
17 | resource_listing = simplejson.load(f)
18 | validate_swagger_schema(dir_path, resource_listing)
19 |
20 |
21 | def test_proper_error_on_missing_api_declaration():
22 | with pytest.raises(ValidationError) as exc:
23 | dir_path = 'tests/sample_schemas/missing_api_declaration/'.replace('/', os.path.sep)
24 | with open(os.path.join(dir_path, API_DOCS_FILENAME)) as f:
25 | resource_listing = simplejson.load(f)
26 | validate_swagger_schema(dir_path, resource_listing)
27 |
28 | assert os.path.basename(dir_path) in str(exc.value)
29 | assert os.path.basename('missing.json') in str(exc.value)
30 |
--------------------------------------------------------------------------------
/tests/sample_schemas/yaml_app/defs.yaml:
--------------------------------------------------------------------------------
1 | definitions:
2 | standard_response:
3 | type: "object"
4 | required:
5 | - "raw_response"
6 | - "logging_info"
7 | additionalProperties: false
8 | properties:
9 | raw_response:
10 | type: "string"
11 | logging_info:
12 | type: "object"
13 | body_model:
14 | type: "object"
15 | required:
16 | - "foo"
17 | additionalProperties: false
18 | properties:
19 | foo:
20 | type: "string"
21 | bar:
22 | type: "string"
23 | array_content_model:
24 | required:
25 | - "enum_value"
26 | properties:
27 | enum_value:
28 | type: "string"
29 | enum:
30 | - "good_enum_value"
31 |
32 | operations:
33 | post:
34 | parameters:
35 | - name: path_arg
36 | in: path
37 | type: string
38 | required: true
39 | - name: request
40 | in: body
41 | required: true
42 | schema:
43 | $ref: "#/definitions/body_model"
44 | responses:
45 | default:
46 | description: test response
47 | schema:
48 | $ref: "#/definitions/standard_response"
49 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. pyramid_swagger documentation master file, created by
2 | sphinx-quickstart on Mon May 12 13:42:31 2014.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to pyramid_swagger's documentation!
7 | ===========================================
8 |
9 | This project offers convenient tools for using `Swagger `_ to define and validate
10 | your interfaces in a `Pyramid `_ webapp.
11 |
12 | **You must supply** a working Pyramid application, and a Swagger schema describing your application's interface. In return, pyramid_swagger will provide:
13 |
14 | * Request and response validation
15 |
16 | * Swagger spec validation
17 |
18 | * Automatically serving the swagger schema to interested clients (e.g. `Swagger UI `_)
19 |
20 | pyramid_swagger works for both the 1.2 and 2.0 Swagger specifications, although users are strongly encouraged to use 2.0 going forward.
21 |
22 | Contents:
23 |
24 | .. toctree::
25 | :maxdepth: 1
26 |
27 | what_is_swagger
28 | quickstart
29 | changelog
30 | configuration
31 | migrating_to_swagger_20
32 | external_resources
33 | glossary
34 |
--------------------------------------------------------------------------------
/tests/acceptance/invalid_file_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import base64
5 |
6 | import pytest
7 | from webtest import TestApp as App
8 |
9 | from pyramid_swagger.tween import SwaggerFormat
10 | from tests.acceptance.app import main
11 |
12 |
13 | @pytest.fixture
14 | def settings():
15 | dir_path = 'tests/sample_schemas/'
16 | return {
17 | 'pyramid_swagger.schema_file': 'swagger.txt',
18 | 'pyramid_swagger.schema_directory': dir_path,
19 | 'pyramid_swagger.enable_request_validation': True,
20 | 'pyramid_swagger.enable_swagger_spec_validation': True,
21 | }
22 |
23 |
24 | @pytest.fixture
25 | def yaml_app():
26 | return SwaggerFormat(format='base64',
27 | to_wire=base64.b64encode,
28 | to_python=base64.b64decode,
29 | validate=base64.b64decode,
30 | description='base64')
31 |
32 |
33 | def test_invalid_file_extension(settings, yaml_app):
34 | """Fixture for setting up a Swagger 2.0 version of the test testapp."""
35 | settings['pyramid_swagger.swagger_versions'] = ['2.0']
36 | settings['pyramid_swagger.user_formats'] = [yaml_app]
37 |
38 | with pytest.raises(Exception):
39 | App(main({}, **settings))
40 |
--------------------------------------------------------------------------------
/pyramid_swagger/spec.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Methods to help validate a given JSON document against the Swagger Spec.
4 | """
5 | from __future__ import absolute_import
6 |
7 | import os
8 |
9 | import swagger_spec_validator
10 | from jsonschema.exceptions import ValidationError
11 | from six.moves.urllib import parse as urlparse
12 | from six.moves.urllib.request import pathname2url
13 |
14 | from pyramid_swagger.exceptions import wrap_exception
15 |
16 | API_DOCS_FILENAME = 'api_docs.json'
17 |
18 |
19 | @wrap_exception(ValidationError)
20 | def validate_swagger_schema(schema_dir, resource_listing):
21 | """Validate the structure of Swagger schemas against the spec.
22 |
23 | **Valid only for Swagger v1.2 spec**
24 |
25 | Note: It is possible that resource_listing is not present in
26 | the schema_dir. The path is passed in the call so that ssv
27 | can fetch the api-declaration files from the path.
28 |
29 | :param resource_listing: Swagger Spec v1.2 resource listing
30 | :type resource_listing: dict
31 | :param schema_dir: A path to Swagger spec directory
32 | :type schema_dir: string
33 | :raises: :py:class:`swagger_spec_validator.SwaggerValidationError`
34 | """
35 | schema_filepath = os.path.join(schema_dir, API_DOCS_FILENAME)
36 | swagger_spec_validator.validator12.validate_spec(
37 | resource_listing,
38 | urlparse.urljoin('file:', pathname2url(os.path.abspath(schema_filepath))),
39 | )
40 |
--------------------------------------------------------------------------------
/tests/sample_schemas/good_app/echo_date.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "0.1",
3 | "swaggerVersion": "1.2",
4 | "basePath": "http://localhost:9999/echo_date",
5 | "apis": [
6 | {
7 | "path": "/echo_date",
8 | "operations": [
9 | {
10 | "method": "POST",
11 | "nickname": "echo_date",
12 | "type": "string",
13 | "parameters": [
14 | {
15 | "name": "body",
16 | "paramType": "body",
17 | "type": "object_with_formats"
18 | }
19 | ]
20 | }
21 | ]
22 | },
23 | {
24 | "path": "/echo_date_json_renderer",
25 | "operations": [
26 | {
27 | "description": "This endpoint is used in tests/acceptance/request_test.py and requires to be identical to /echo_date endpoint",
28 | "method": "POST",
29 | "nickname": "echo_date",
30 | "parameters": [
31 | {
32 | "name": "body",
33 | "paramType": "body",
34 | "type": "object_with_formats"
35 | }
36 | ],
37 | "type": "string"
38 | }
39 | ]
40 | }
41 | ],
42 | "models": {
43 | "object_with_formats": {
44 | "id": "object_with_formats",
45 | "properties": {
46 | "date": {
47 | "type": "string",
48 | "format": "date"
49 | }
50 | },
51 | "required": [
52 | "date"
53 | ]
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tests/sample_schemas/recursive_app/internal/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "Bug demo",
5 | "description": "Recursive object definitions cause problems",
6 | "version": "0.1.0"
7 | },
8 | "definitions": {
9 | "widget": {
10 | "description": "A widget which may have multiple generations of child widgets",
11 | "properties": {
12 | "name": {
13 | "type": "string",
14 | "minLength": 1,
15 | "maxLength": 50
16 | },
17 | "children": {
18 | "type": "array",
19 | "items": {
20 | "$ref":"#/definitions/widget"
21 | }
22 | }
23 | },
24 | "type": "object"
25 | }
26 | },
27 | "paths": {
28 | "/resources/widget/{code}" : {
29 | "parameters" : [
30 | {
31 | "name": "code",
32 | "description": "The CODE value for a widget",
33 | "in": "path",
34 | "type": "string",
35 | "required": true
36 | }
37 | ],
38 | "get": {
39 | "summary": "View a single widget",
40 | "produces": ["application/json"],
41 | "parameters": [],
42 | "responses": {
43 | "200": {
44 | "description":"Widget local content (without parents or descendents)",
45 | "schema": {"$ref":"#/definitions/widget"}
46 | },
47 | "404": {"description": "The widget does not exist"}
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import io
5 | import os
6 |
7 | from setuptools import find_packages
8 | from setuptools import setup
9 |
10 |
11 | base_dir = os.path.dirname(__file__)
12 | about = {}
13 | with open(os.path.join(base_dir, "pyramid_swagger", "__about__.py")) as f:
14 | exec(f.read(), about)
15 |
16 | with io.open(os.path.join(base_dir, "README.rst"), encoding='utf-8') as f:
17 | long_description = f.read()
18 |
19 | setup(
20 | name=about['__title__'],
21 | version=about['__version__'],
22 |
23 | description=about['__summary__'],
24 | long_description=long_description,
25 | license=about['__license__'],
26 | url=about["__uri__"],
27 |
28 | author=about['__author__'],
29 | author_email=about['__email__'],
30 |
31 | classifiers=[
32 | 'Development Status :: 5 - Production/Stable',
33 |
34 | 'Intended Audience :: Developers',
35 | 'Topic :: Software Development :: Libraries :: Python Modules',
36 |
37 | 'Programming Language :: Python :: 3.8',
38 | 'Programming Language :: Python :: 3.10',
39 |
40 | 'License :: OSI Approved :: BSD License',
41 | ],
42 | keywords='pyramid swagger validation',
43 | packages=find_packages(exclude=["contrib", "docs", "tests*"]),
44 | include_package_data=True,
45 | install_requires=[
46 | 'bravado-core >= 4.8.4',
47 | 'jsonschema >= 3.0.0',
48 | 'pyramid',
49 | 'simplejson',
50 | ],
51 | )
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Scott Triglia
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 | * Redistributions of source code must retain the above copyright
7 | notice, this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright
9 | notice, this list of conditions and the following disclaimer in the
10 | documentation and/or other materials provided with the distribution.
11 | * Neither the name of the nor the
12 | names of its contributors may be used to endorse or promote products
13 | derived from this software without specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/tests/acceptance/recursive_app_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import json
5 |
6 | import pytest
7 | import yaml
8 | from six import BytesIO
9 | from webtest import TestApp as App
10 |
11 | from tests.acceptance.app import main
12 |
13 |
14 | DESERIALIZERS = {
15 | 'json': lambda r: json.loads(r.body.decode('utf-8')),
16 | 'yaml': lambda r: yaml.safe_load(BytesIO(r.body)),
17 | }
18 |
19 |
20 | @pytest.fixture
21 | def settings():
22 | dir_path = 'tests/sample_schemas/recursive_app/external/'
23 | return {
24 | 'pyramid_swagger.schema_directory': dir_path,
25 | 'pyramid_swagger.enable_request_validation': True,
26 | 'pyramid_swagger.enable_swagger_spec_validation': True,
27 | 'pyramid_swagger.swagger_versions': ['2.0']
28 | }
29 |
30 |
31 | @pytest.fixture
32 | def test_app_deref(settings):
33 | """Fixture for setting up a Swagger 2.0 version of the test test_app
34 | test app serves swagger schemas with refs dereferenced."""
35 | settings['pyramid_swagger.dereference_served_schema'] = True
36 | return App(main({}, **settings))
37 |
38 |
39 | @pytest.mark.parametrize('schema_format', ['json'])
40 | def test_dereferenced_swagger_schema_bravado_client(
41 | schema_format,
42 | test_app_deref,
43 | ):
44 | from bravado.client import SwaggerClient
45 |
46 | response = test_app_deref.get('/swagger.{0}'.format(schema_format))
47 | deserializer = DESERIALIZERS[schema_format]
48 | specs = deserializer(response)
49 |
50 | SwaggerClient.from_spec(specs)
51 |
--------------------------------------------------------------------------------
/tests/acceptance/config_ini_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import os.path
5 |
6 | import pytest
7 | from pyramid.paster import get_appsettings
8 | from webtest import TestApp as App
9 |
10 | from pyramid_swagger.tween import get_swagger_versions
11 | from pyramid_swagger.tween import load_settings
12 | from tests.acceptance.app import main
13 |
14 |
15 | @pytest.fixture
16 | def ini_app():
17 | settings = get_appsettings(os.path.join(os.path.dirname(__file__), 'app', 'config.ini'), name='main')
18 | # Swagger 1.2 tests are broken. Swagger 1.2 is deprecated and thus we have no plans to fix these tests,
19 | # so removing them here.
20 | settings["pyramid_swagger.swagger_versions"] = "2.0"
21 | return App(main({}, **settings))
22 |
23 |
24 | def test_load_ini_settings(ini_app):
25 | registry = ini_app.app.registry
26 | settings = load_settings(registry)
27 |
28 | # Make sure these settings are booleans
29 | assert settings.validate_request is True
30 | assert settings.validate_response is False
31 | assert settings.validate_path is True
32 | assert settings.exclude_routes == {'/undefined/first', '/undefined/second'}
33 | assert settings.prefer_20_routes == {'/sample'}
34 | assert settings.response_validation_exclude_routes == {'/exclude_response/first', '/exclude_response/second'}
35 |
36 |
37 | def test_get_swagger_versions(ini_app):
38 | settings = ini_app.app.registry.settings
39 | swagger_versions = get_swagger_versions(settings)
40 | # Swagger 1.2 tests are broken. Swagger 1.2 is deprecated and thus we have no plans to fix these tests,
41 | # so removing them here.
42 | assert swagger_versions == {'2.0'}
43 |
--------------------------------------------------------------------------------
/pyramid_swagger/exceptions.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import sys
5 |
6 | import six
7 | from pyramid.httpexceptions import HTTPBadRequest
8 | from pyramid.httpexceptions import HTTPInternalServerError
9 | from pyramid.httpexceptions import HTTPNotFound
10 | from pyramid.httpexceptions import HTTPUnauthorized
11 |
12 |
13 | class RequestValidationError(HTTPBadRequest):
14 | def __init__(self, *args, **kwargs):
15 | self.child = kwargs.pop('child', None)
16 | super(RequestValidationError, self).__init__(*args, **kwargs)
17 |
18 |
19 | class RequestAuthenticationError(HTTPUnauthorized):
20 | def __init__(self, *args, **kwargs):
21 | self.child = kwargs.pop('child', None)
22 | super(RequestAuthenticationError, self).__init__(*args, **kwargs)
23 |
24 |
25 | class PathNotFoundError(HTTPNotFound):
26 | def __init__(self, *args, **kwargs):
27 | self.child = kwargs.pop('child', None)
28 | super(PathNotFoundError, self).__init__(*args, **kwargs)
29 |
30 |
31 | class ResponseValidationError(HTTPInternalServerError):
32 | def __init__(self, *args, **kwargs):
33 | self.child = kwargs.pop('child', None)
34 | super(ResponseValidationError, self).__init__(*args, **kwargs)
35 |
36 |
37 | def wrap_exception(exception_class):
38 | def generic_exception(method):
39 | def wrapper(*args, **kwargs):
40 | try:
41 | method(*args, **kwargs)
42 | except Exception as e:
43 | six.reraise(
44 | exception_class,
45 | exception_class(str(e)),
46 | sys.exc_info()[2])
47 | return wrapper
48 | return generic_exception
49 |
--------------------------------------------------------------------------------
/tests/sample_schemas/prefer_20_routes_app/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "Title was not specified",
5 | "version": "0.1"
6 | },
7 | "produces": ["application/json"],
8 | "paths": {
9 | "/sample/{path_arg}/resource": {
10 | "get": {
11 | "responses": {
12 | "200": {
13 | "description": "Return a standard_response",
14 | "schema": {
15 | "$ref": "#/definitions/standard_response"
16 | }
17 | }
18 | },
19 | "description": "",
20 | "operationId": "standard",
21 | "parameters": [
22 | {
23 | "in": "path",
24 | "name": "path_arg",
25 | "required": true,
26 | "type": "string",
27 | "enum": ["path_arg1", "path_arg2"]
28 | },
29 | {
30 | "in": "query",
31 | "name": "required_arg",
32 | "required": true,
33 | "type": "string"
34 | },
35 | {
36 | "in": "query",
37 | "name": "optional_arg",
38 | "required": false,
39 | "type": "string"
40 | }
41 | ]
42 | }
43 | }
44 | },
45 | "host": "localhost:9999",
46 | "schemes": [
47 | "http"
48 | ],
49 | "definitions": {
50 | "standard_response": {
51 | "type": "object",
52 | "required": [
53 | "raw_response",
54 | "logging_info"
55 | ],
56 | "additionalProperties": false,
57 | "properties": {
58 | "raw_response": {
59 | "type": "string"
60 | },
61 | "logging_info": {
62 | "type": "object"
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/acceptance/format_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import base64
5 |
6 | import pytest
7 | from webtest import TestApp as App
8 |
9 | from pyramid_swagger.tween import SwaggerFormat
10 | from tests.acceptance.app import main
11 |
12 |
13 | @pytest.fixture
14 | def settings():
15 | dir_path = 'tests/sample_schemas/user_format/'
16 | return {
17 | 'pyramid_swagger.schema_directory': dir_path,
18 | 'pyramid_swagger.enable_request_validation': True,
19 | 'pyramid_swagger.enable_swagger_spec_validation': True,
20 | }
21 |
22 |
23 | @pytest.fixture
24 | def user_format():
25 | return SwaggerFormat(format='base64',
26 | to_wire=base64.b64encode,
27 | to_python=base64.b64decode,
28 | validate=base64.b64decode,
29 | description='base64')
30 |
31 |
32 | @pytest.fixture
33 | def testapp_with_base64(settings, user_format):
34 | """Fixture for setting up a Swagger 2.0 version of the test testapp."""
35 | settings['pyramid_swagger.swagger_versions'] = ['2.0']
36 | settings['pyramid_swagger.user_formats'] = [user_format]
37 | return App(main({}, **settings))
38 |
39 |
40 | def test_user_format_happy_case(testapp_with_base64):
41 | response = testapp_with_base64.get('/sample/path_arg1/resource',
42 | params={'required_arg': 'MQ=='},)
43 | assert response.status_code == 200
44 |
45 |
46 | def test_user_format_failure_case(testapp_with_base64):
47 | # 'MQ' is not a valid base64 encoded string.
48 | with pytest.raises(Exception):
49 | testapp_with_base64.get('/sample/path_arg1/resource',
50 | params={'required_arg': 'MQ'},)
51 |
--------------------------------------------------------------------------------
/tests/load_schema_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import mock
5 | import pytest
6 |
7 | from pyramid_swagger import load_schema
8 |
9 |
10 | @pytest.fixture
11 | def mock_validator():
12 | return mock.Mock(spec=['validate', 'is_type'])
13 |
14 |
15 | def test_required_validator_bool_is_missing():
16 | schema = {'paramType': 'body', 'name': 'body'}
17 | errors = list(load_schema.required_validator(None, True, {}, schema))
18 | assert len(errors) == 1
19 | assert 'body is required' in str(errors[0])
20 |
21 |
22 | def test_required_validator_bool_is_present():
23 | schema = {'paramType': 'body', 'name': 'body'}
24 | inst = {'foo': 1}
25 | errors = list(load_schema.required_validator(None, True, inst, schema))
26 | assert len(errors) == 0
27 |
28 |
29 | def test_required_validator_bool_not_required():
30 | schema = {'paramType': 'body', 'name': 'body'}
31 | errors = list(load_schema.required_validator(None, False, {}, schema))
32 | assert len(errors) == 0
33 |
34 |
35 | def test_required_validator_list(mock_validator):
36 | required = ['one', 'two']
37 | errors = list(load_schema.required_validator(
38 | mock_validator, required, {}, {}))
39 | assert len(errors) == 2
40 |
41 |
42 | def test_type_validator_skips_File():
43 | schema = {'paramType': 'form', 'type': 'File'}
44 | errors = list(load_schema.type_validator(None, "File", '', schema))
45 | assert len(errors) == 0
46 |
47 |
48 | @mock.patch('pyramid_swagger.load_schema._draft3_type_validator')
49 | def test_type_validator_calls_draft3_type_validator_when_not_File(mock_type_draft3):
50 | schema = {'paramType': 'form', 'type': 'number'}
51 | list(load_schema.type_validator(None, "number", 99, schema))
52 | assert mock_type_draft3.call_count == 1
53 |
--------------------------------------------------------------------------------
/pyramid_swagger/renderer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | This model contains the main factory of renderers to use while dealing with Swagger 2.0 endpoints.
4 | """
5 | from __future__ import absolute_import
6 |
7 | from functools import partial
8 |
9 | from bravado_core.exception import MatchingResponseNotFound
10 | from bravado_core.exception import SwaggerMappingError
11 | from bravado_core.marshal import marshal_schema_object
12 | from bravado_core.response import get_response_spec
13 | from pyramid.renderers import JSON
14 |
15 |
16 | class PyramidSwaggerRendererFactory(object):
17 | def __init__(self, renderer_factory=JSON()):
18 | self.renderer_factory = renderer_factory
19 |
20 | def _marshal_object(self, request, response_object):
21 | # operation attribute is injected by validator_tween in case the endpoint is served by Swagger 2.0 specs
22 | operation = getattr(request, 'operation', None)
23 |
24 | if not operation:
25 | # If the request is not served by Swagger2.0 endpoint _marshal_object is NO_OP
26 | return response_object
27 |
28 | try:
29 | response_spec = get_response_spec(
30 | status_code=request.response.status_code,
31 | op=request.operation,
32 | )
33 | return marshal_schema_object(
34 | swagger_spec=request.registry.settings['pyramid_swagger.schema20'],
35 | schema_object_spec=response_spec['schema'],
36 | value=response_object,
37 | )
38 | except (MatchingResponseNotFound, SwaggerMappingError, KeyError):
39 | # marshaling process failed
40 | return response_object
41 |
42 | def _render(self, external_renderer, value, system):
43 | value = self._marshal_object(system['request'], value)
44 | return external_renderer(value, system)
45 |
46 | def __call__(self, info):
47 | return partial(self._render, self.renderer_factory(info))
48 |
--------------------------------------------------------------------------------
/tests/acceptance/prefer_20_routes_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import pytest
5 | from webtest import TestApp as App
6 |
7 | from tests.acceptance.app import main
8 |
9 |
10 | @pytest.fixture
11 | def settings():
12 | dir_path = 'tests/sample_schemas/prefer_20_routes_app/'
13 | return {
14 | 'pyramid_swagger.schema_directory': dir_path,
15 | 'pyramid_swagger.enable_request_validation': True,
16 | 'pyramid_swagger.enable_swagger_spec_validation': True,
17 | # Swagger 1.2 tests are broken. Swagger 1.2 is deprecated and thus we have no plans to fix these tests,
18 | # so removing them here.
19 | 'pyramid_swagger.swagger_versions': '2.0',
20 | }
21 |
22 |
23 | @pytest.fixture
24 | def test_app_with_no_prefer_conf(settings):
25 | """Fixture for setting up a Swagger 2.0 version with no
26 | `prefer_20_routes` option added to settings."""
27 | return App(main({}, **settings))
28 |
29 |
30 | @pytest.fixture
31 | def test_app_with_prefer_conf(settings):
32 | """Fixture for setting up a Swagger 2.0 version with a particular route
33 | `standard` added to `prefer_20_routes` option."""
34 | settings['pyramid_swagger.prefer_20_routes'] = ['standard']
35 | return App(main({}, **settings))
36 |
37 |
38 | def test_failure_with_no_prefer_config_case(test_app_with_no_prefer_conf):
39 | """The second get call should fail as it is not covered in v2.0 spec
40 | """
41 | response = test_app_with_no_prefer_conf.get('/sample/path_arg1/resource',
42 | params={'required_arg': 'a'},)
43 | assert response.status_code == 200
44 | with pytest.raises(Exception):
45 | test_app_with_no_prefer_conf.get(
46 | '/sample/nonstring/1/1.1/true', params={},)
47 |
48 |
49 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
50 | def test_success_with_prefer_config_case(test_app_with_prefer_conf):
51 | response = test_app_with_prefer_conf.get('/sample/path_arg1/resource',
52 | params={'required_arg': 'a'},)
53 | assert response.status_code == 200
54 | response = test_app_with_prefer_conf.get(
55 | '/sample/nonstring/1/1.1/true', params={},)
56 | assert response.status_code == 200
57 |
--------------------------------------------------------------------------------
/pyramid_swagger/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Import this module to add the validation tween to your pyramid app.
4 | """
5 | from __future__ import absolute_import
6 |
7 | import pyramid
8 |
9 | from pyramid_swagger.api import build_swagger_20_swagger_schema_views
10 | from pyramid_swagger.api import register_api_doc_endpoints
11 | from pyramid_swagger.ingest import get_swagger_schema
12 | from pyramid_swagger.ingest import get_swagger_spec
13 | from pyramid_swagger.renderer import PyramidSwaggerRendererFactory
14 | from pyramid_swagger.tween import get_swagger_versions
15 | from pyramid_swagger.tween import SWAGGER_12
16 | from pyramid_swagger.tween import SWAGGER_20
17 |
18 |
19 | def includeme(config):
20 | """
21 | :type config: :class:`pyramid.config.Configurator`
22 | """
23 | settings = config.registry.settings
24 | swagger_versions = get_swagger_versions(settings)
25 |
26 | # for rendering /swagger.yaml
27 | config.add_renderer(
28 | 'yaml', 'pyramid_swagger.api.YamlRendererFactory',
29 | )
30 |
31 | # Add the SwaggerSchema to settings to make it available to the validation
32 | # tween and `register_api_doc_endpoints`
33 | settings['pyramid_swagger.schema12'] = None
34 | settings['pyramid_swagger.schema20'] = None
35 |
36 | # Store under two keys so that 1.2 and 2.0 can co-exist.
37 | if SWAGGER_12 in swagger_versions:
38 | settings['pyramid_swagger.schema12'] = get_swagger_schema(settings)
39 |
40 | if SWAGGER_20 in swagger_versions:
41 | settings['pyramid_swagger.schema20'] = get_swagger_spec(settings)
42 |
43 | config.add_tween(
44 | "pyramid_swagger.tween.validation_tween_factory",
45 | under=pyramid.tweens.EXCVIEW
46 | )
47 |
48 | config.add_renderer('pyramid_swagger', PyramidSwaggerRendererFactory())
49 |
50 | if settings.get('pyramid_swagger.enable_api_doc_views', True):
51 | if SWAGGER_12 in swagger_versions:
52 | register_api_doc_endpoints(
53 | config,
54 | settings['pyramid_swagger.schema12'].get_api_doc_endpoints())
55 |
56 | if SWAGGER_20 in swagger_versions:
57 | register_api_doc_endpoints(
58 | config,
59 | build_swagger_20_swagger_schema_views(config),
60 | base_path=settings.get('pyramid_swagger.base_path_api_docs', ''))
61 |
--------------------------------------------------------------------------------
/tests/sample_schemas/relative_ref/dereferenced_swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "host": "localhost:9999",
3 | "info": {
4 | "title": "Title was not specified",
5 | "version": "0.1"
6 | },
7 | "parameters": {
8 | "lfile:parameters..common.json|..path_arg": {
9 | "enum": [
10 | "path_arg1",
11 | "path_arg2"
12 | ],
13 | "in": "path",
14 | "name": "path_arg",
15 | "required": true,
16 | "type": "string"
17 | }
18 | },
19 | "paths": {
20 | "/no_models": {
21 | "get": {
22 | "description": "",
23 | "operationId": "no_models_get",
24 | "responses": {
25 | "200": {
26 | "$ref": "#/responses/lfile:responses..common.json|..200"
27 | }
28 | }
29 | }
30 | },
31 | "/sample/{path_arg}/resource": {
32 | "get": {
33 | "description": "",
34 | "operationId": "standard",
35 | "parameters": [
36 | {
37 | "$ref": "#/parameters/lfile:parameters..common.json|..path_arg"
38 | }
39 | ],
40 | "responses": {
41 | "200": {
42 | "$ref": "#/responses/lfile:responses..common.json|..200"
43 | }
44 | }
45 | }
46 | }
47 | },
48 | "produces": [
49 | "application/json"
50 | ],
51 | "responses": {
52 | "lfile:responses..common.json|..200": {
53 | "description": "Return a standard_response",
54 | "schema": {
55 | "additionalProperties": false,
56 | "properties": {
57 | "logging_info": {
58 | "type": "object"
59 | },
60 | "raw_response": {
61 | "type": "string"
62 | }
63 | },
64 | "required": [
65 | "raw_response",
66 | "logging_info"
67 | ],
68 | "type": "object"
69 | }
70 | }
71 | },
72 | "schemes": [
73 | "http"
74 | ],
75 | "swagger": "2.0"
76 | }
77 |
--------------------------------------------------------------------------------
/tests/sample_schemas/user_format/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "Title was not specified",
5 | "version": "0.1"
6 | },
7 | "produces": ["application/json"],
8 | "paths": {
9 | "/sample/{path_arg}/resource": {
10 | "get": {
11 | "responses": {
12 | "200": {
13 | "description": "Return a standard_response",
14 | "schema": {
15 | "$ref": "#/definitions/standard_response"
16 | }
17 | }
18 | },
19 | "description": "",
20 | "operationId": "standard",
21 | "parameters": [
22 | {
23 | "in": "path",
24 | "name": "path_arg",
25 | "required": true,
26 | "type": "string",
27 | "enum": ["path_arg1", "path_arg2"]
28 | },
29 | {
30 | "in": "query",
31 | "name": "required_arg",
32 | "required": true,
33 | "type": "string",
34 | "format": "base64"
35 | },
36 | {
37 | "in": "query",
38 | "name": "optional_arg",
39 | "required": false,
40 | "type": "string"
41 | }
42 | ]
43 | }
44 | }
45 | },
46 | "host": "localhost:9999",
47 | "schemes": [
48 | "http"
49 | ],
50 | "definitions": {
51 | "standard_response": {
52 | "type": "object",
53 | "required": [
54 | "raw_response",
55 | "logging_info"
56 | ],
57 | "additionalProperties": false,
58 | "properties": {
59 | "raw_response": {
60 | "type": "string"
61 | },
62 | "logging_info": {
63 | "type": "object"
64 | }
65 | }
66 | },
67 | "body_model": {
68 | "type": "object",
69 | "required": [
70 | "foo"
71 | ],
72 | "additionalProperties": false,
73 | "properties": {
74 | "foo": {
75 | "type": "string"
76 | },
77 | "bar": {
78 | "type": "string"
79 | }
80 | }
81 | },
82 | "array_content_model": {
83 | "required": [
84 | "enum_value"
85 | ],
86 | "properties": {
87 | "enum_value": {
88 | "type": "string",
89 | "enum": [
90 | "good_enum_value"
91 | ]
92 | }
93 | }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/docs/migrating_to_swagger_20.rst:
--------------------------------------------------------------------------------
1 | Migrating to Swagger 2.0
2 | ========================
3 |
4 | So you're using pyramid_swagger with Swagger 1.2 and now it is time to upgrade to Swagger 2.0.
5 |
6 | Just set the version of Swagger to support via configuration.
7 |
8 | .. code-block:: ini
9 |
10 | [app:main]
11 | pyramid_swagger.swagger_versions = ['2.0']
12 |
13 | If you would like to continue servicing Swagger 1.2 clients, pyramid_swagger has you covered.
14 |
15 | .. code-block:: ini
16 |
17 | [app:main]
18 | pyramid_swagger.swagger_versions = ['1.2', '2.0']
19 |
20 | .. note::
21 |
22 | When both versions of Swagger are supported, all requests are validated against the 2.0 version of the schema only.
23 | Make sure that your 1.2 and 2.0 schemas define an identical set of APIs.
24 |
25 | If you're not using an ini file, configuration in Python also works.
26 |
27 | .. code-block:: python
28 |
29 | def main(global_config, **settings):
30 | # ...
31 | settings['pyramid_swagger.swagger_versions'] = ['2.0']
32 | # ...and so on with the other settings...
33 | config = Configurator(settings=settings)
34 | config.include('pyramid_swagger')
35 |
36 | Next, create a Swagger 2.0 version of your swagger schema. There are some great resources to help you with the conversion process.
37 |
38 | * `Swagger 1.2 to 2.0 Migration Guide `_
39 | * `Swagger Converter `_
40 | * `Swagger 2.0 Specification `_
41 |
42 | Finally, place your Swagger 2.0 schema ``swagger.json`` file in the same directory as your Swagger 1.2 schema and you're ready to go.
43 |
44 | .. _prefer20migration:
45 |
46 | Incremental Migration
47 | ---------------------
48 |
49 | If your v1.2 spec is too large and you are looking to migrate specs incrementally, then the below
50 | config can be useful.
51 |
52 | .. code-block:: ini
53 |
54 | [app:main]
55 | pyramid_swagger.prefer_20_routes = ['route_foo']
56 |
57 | .. note::
58 |
59 | The above config is read only when both `['1.2', '2.0']` are present in `swagger_versions` config. If that
60 | is the case and the request's route is present in `prefer_20_routes`, ONLY then the request is served through
61 | swagger 2.0 otherwise through 1.2. The only exception is either the config is not defined at all or both of the
62 | swagger versions are not enabled, in any of these cases, v2.0 is preferred (as mentioned in above note).
63 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | :PyPI: https://pypi.python.org/pypi/pyramid_swagger
2 | :Documentation: http://pyramid-swagger.readthedocs.org/en/latest/
3 | :Source: https://github.com/striglia/pyramid_swagger
4 | :License: Copyright © 2014 Scott Triglia under the `BSD 3-clause `_
5 | :Build status:
6 | .. image:: https://travis-ci.org/striglia/pyramid_swagger.png?branch=master
7 | :target: https://travis-ci.org/striglia/pyramid_swagger?branch=master
8 | :alt: Travis CI
9 | .. image:: https://ci.appveyor.com/api/projects/status/ufmlmpwy1vj3yjgk/branch/master?svg=true
10 | :target: https://ci.appveyor.com/project/striglia/pyramid-swagger
11 | :alt: Appveyor (Windows CI)
12 | :Current coverage on master:
13 | .. image:: https://coveralls.io/repos/striglia/pyramid_swagger/badge.png
14 | :target: https://coveralls.io/r/striglia/pyramid_swagger
15 | :Persistent chat for questions:
16 | .. image:: https://badges.gitter.im/Join%20Chat.svg
17 | :alt: Join the chat at https://gitter.im/striglia/pyramid_swagger
18 | :target: https://gitter.im/striglia/pyramid_swagger?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
19 |
20 |
21 | pyramid_swagger
22 | ===============
23 |
24 | This project offers convenient tools for using Swagger to define and validate
25 | your interfaces in a Pyramid webapp.
26 |
27 | Full documentation is available at http://pyramid-swagger.readthedocs.org/.
28 |
29 |
30 | How to contribute
31 | -----------------
32 |
33 | #. Fork this repository on Github: https://help.github.com/articles/fork-a-repo/
34 | #. Clone your forked repository: https://help.github.com/articles/cloning-a-repository/
35 | #. Make a feature branch for your changes:
36 |
37 | ::
38 |
39 | git remote add upstream https://github.com/Yelp/pyramid_swagger.git
40 | git fetch upstream
41 | git checkout upstream/master -b my-feature-branch
42 |
43 | #. Create and activate the virtual environment, this will provide you with all the
44 | libraries and tools necessary for pyramid_swagger development:
45 |
46 | ::
47 |
48 | make
49 | source .activate.sh
50 |
51 | #. Make sure the test suite works before you start:
52 |
53 | ::
54 |
55 | tox -e py38 # Note: use py310 for Python 3.10, see tox.ini for possible values
56 |
57 | #. Commit patches: http://gitref.org/basic/
58 | #. Push to github: ``git pull && git push origin``
59 | #. Send a pull request: https://help.github.com/articles/creating-a-pull-request/
60 |
61 |
62 | Running a single test
63 | *********************
64 |
65 | Make sure you have activated the virtual environment (see above).
66 |
67 | ::
68 |
69 | py.test -vvv tests/tween_test.py::test_response_properties
70 |
--------------------------------------------------------------------------------
/tests/sample_schemas/prefer_20_routes_app/other_sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "0.1",
3 | "swaggerVersion": "1.2",
4 | "basePath": "http://localhost:9999/sample",
5 | "apis": [
6 | {
7 | "path": "/sample/{path_arg}/resource",
8 | "operations": [
9 | {
10 | "method": "GET",
11 | "nickname": "standard",
12 | "type": "standard_response",
13 | "parameters": [
14 | {
15 | "paramType": "path",
16 | "name": "path_arg",
17 | "type": "string",
18 | "enum": ["path_arg1", "path_arg2"],
19 | "required": true
20 | },
21 | {
22 | "paramType": "query",
23 | "name": "required_arg",
24 | "type": "string",
25 | "required": true
26 | }
27 | ]
28 | }
29 | ]
30 | },
31 | {
32 | "path": "/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}",
33 | "operations": [
34 | {
35 | "method": "GET",
36 | "nickname": "sample_nonstring",
37 | "type": "void",
38 | "parameters": [
39 | {
40 | "paramType": "path",
41 | "name": "int_arg",
42 | "type": "integer",
43 | "required": true
44 | },
45 | {
46 | "paramType": "path",
47 | "name": "float_arg",
48 | "type": "number",
49 | "format": "float",
50 | "required": true
51 | },
52 | {
53 | "paramType": "path",
54 | "name": "boolean_arg",
55 | "type": "boolean",
56 | "required": true
57 | }
58 | ]
59 | }
60 | ]
61 | }
62 | ],
63 | "models": {
64 | "object": {
65 | "id": "object",
66 | "properties": { }
67 | },
68 | "standard_response": {
69 | "id": "standard_response",
70 | "type": "object",
71 | "required": [ "raw_response", "logging_info" ],
72 | "additionalProperties": false,
73 | "properties": {
74 | "raw_response": {
75 | "type": "string"
76 | },
77 | "logging_info": {
78 | "$ref": "object"
79 | }
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/pyramid_swagger/model.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | The core model we use to represent the entire ingested swagger schema for this
4 | service.
5 | """
6 | from __future__ import absolute_import
7 |
8 | import re
9 | from collections import namedtuple
10 |
11 |
12 | PyramidEndpoint = namedtuple(
13 | 'PyramidEndpoint',
14 | 'path route_name view renderer')
15 |
16 |
17 | class PathNotMatchedError(Exception):
18 | """Raised when a SwaggerSchema object is given a request it cannot match
19 | against its stored schema."""
20 | pass
21 |
22 |
23 | class SwaggerSchema(object):
24 | """
25 | This object contains data structures representing your Swagger schema
26 | and exposes methods for efficiently finding the relevant schemas for a
27 | Pyramid request.
28 |
29 | :param pyramid_endpoints: a list of :class:`PyramidEndpoint` which define
30 | the pyramid endpoints to create for serving the api docs
31 | :param resource_validators: a list of resolvers, one per Swagger resource
32 | :type resource_validators: list of mappings from :class:`RequestMatcher`
33 | to :class:`ValidatorMap`
34 | for every operation in the api specification.
35 | """
36 |
37 | def __init__(self, pyramid_endpoints, resource_validators):
38 | self.pyramid_endpoints = pyramid_endpoints
39 | self.resource_validators = resource_validators
40 |
41 | def validators_for_request(self, request, **kwargs):
42 | """Takes a request and returns a validator mapping for the request.
43 |
44 | :param request: A Pyramid request to fetch schemas for
45 | :type request: :class:`pyramid.request.Request`
46 | :returns: a :class:`pyramid_swagger.load_schema.ValidatorMap` which can
47 | be used to validate `request`
48 | """
49 | for resource_validator in self.resource_validators:
50 | for matcher, validator_map in resource_validator.items():
51 | if matcher.matches(request):
52 | return validator_map
53 |
54 | raise PathNotMatchedError(
55 | 'Could not find the relevant path ({0}) in the Swagger schema. '
56 | 'Perhaps you forgot to add it?'.format(request.path_info)
57 | )
58 |
59 | def get_api_doc_endpoints(self):
60 | return self.pyramid_endpoints
61 |
62 |
63 | def partial_path_match(path1, path2, kwarg_re=r'\{.*\}'):
64 | """Validates if path1 and path2 matches, ignoring any kwargs in the string.
65 |
66 | We need this to ensure we can match Swagger patterns like:
67 | /foo/{id}
68 | against the observed pyramid path
69 | /foo/1
70 |
71 | :param path1: path of a url
72 | :type path1: string
73 | :param path2: path of a url
74 | :type path2: string
75 | :param kwarg_re: regex pattern to identify kwargs
76 | :type kwarg_re: regex string
77 | :returns: boolean
78 | """
79 | split_p1 = path1.split('/')
80 | split_p2 = path2.split('/')
81 | pat = re.compile(kwarg_re)
82 | if len(split_p1) != len(split_p2):
83 | return False
84 | for partial_p1, partial_p2 in zip(split_p1, split_p2):
85 | if pat.match(partial_p1) or pat.match(partial_p2):
86 | continue
87 | if not partial_p1 == partial_p2:
88 | return False
89 | return True
90 |
--------------------------------------------------------------------------------
/tests/model_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Unit tests for the SwaggerSchema class.
4 | """
5 | from __future__ import absolute_import
6 |
7 | import mock
8 | import pytest
9 |
10 | from pyramid_swagger.ingest import compile_swagger_schema
11 | from pyramid_swagger.ingest import get_resource_listing
12 | from pyramid_swagger.model import partial_path_match
13 | from pyramid_swagger.model import PathNotMatchedError
14 |
15 |
16 | @pytest.fixture
17 | def schema():
18 | schema_dir = 'tests/sample_schemas/good_app/'
19 | return compile_swagger_schema(
20 | schema_dir,
21 | get_resource_listing(schema_dir, False)
22 | )
23 |
24 |
25 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
26 | def test_swagger_schema_for_request_different_methods(schema):
27 | """Tests that validators_for_request() checks the request
28 | method."""
29 | # There exists a GET and POST for this endpoint. We should be able to call
30 | # either and have them pass validation.
31 | value = schema.validators_for_request(
32 | request=mock.Mock(
33 | path_info="/sample",
34 | method="GET"
35 | ),
36 | )
37 | assert value.body.schema is None
38 |
39 | value = schema.validators_for_request(
40 | request=mock.Mock(
41 | path_info="/sample",
42 | method="POST",
43 | body={'foo': 1, 'bar': 2},
44 | ),
45 | )
46 | assert value.body.schema == {
47 | 'required': True,
48 | 'name': 'content',
49 | 'paramType': 'body',
50 | 'type': 'body_model',
51 | }
52 |
53 |
54 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
55 | def test_swagger_schema_for_request_not_found(schema):
56 | """Tests that validators_for_request() raises exceptions when
57 | a path is not found.
58 | """
59 | # There exists a GET and POST for this endpoint. We should be able to call
60 | # either and have them pass validation.
61 | with pytest.raises(PathNotMatchedError) as excinfo:
62 | schema.validators_for_request(
63 | request=mock.Mock(
64 | path_info="/does_not_exist",
65 | method="GET"
66 | ),
67 | )
68 | assert '/does_not_exist' in str(excinfo.value)
69 | assert 'Could not find ' in str(excinfo.value)
70 |
71 |
72 | def test_partial_path_match():
73 | assert partial_path_match(
74 | '/v1/bing/forward_unstructured',
75 | '/v1/bing/forward_unstructured'
76 | )
77 | assert partial_path_match(
78 | '/v1/{api_provider}/forward_unstructured',
79 | '/v1/bing/forward_unstructured'
80 | )
81 | assert not partial_path_match(
82 | '/v1/google/forward_unstructured',
83 | '/v1/bing/forward_unstructured'
84 | )
85 |
86 |
87 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
88 | def test_swagger_schema_for_request_virtual_subpath(schema):
89 |
90 | # There exists a GET and POST for this endpoint. We should be able to call
91 | # either and have them pass validation.
92 | value = schema.validators_for_request(
93 | request=mock.Mock(
94 | path="/subpath/sample",
95 | script_name="/subpath",
96 | path_info="/sample",
97 | method="GET"
98 | ),
99 | )
100 | assert value.body.schema is None
101 |
--------------------------------------------------------------------------------
/tests/acceptance/app/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import datetime
5 |
6 | import six
7 | import webob
8 | from pyramid.config import Configurator
9 | from pyramid.view import view_config
10 |
11 |
12 | @view_config(route_name='throw_400', renderer='json')
13 | def throw_error(request):
14 | request.response.status = webob.exc.HTTPBadRequest.code
15 | return dict(error=dict(details='Throwing error!'))
16 |
17 |
18 | @view_config(route_name='standard', renderer='json')
19 | def standard(request, path_arg):
20 | return {
21 | 'raw_response': 'foo',
22 | 'logging_info': {},
23 | }
24 |
25 |
26 | @view_config(route_name='sample_nonstring', renderer='json')
27 | @view_config(route_name='get_with_non_string_query_args', renderer='json')
28 | @view_config(route_name='post_with_primitive_body', renderer='json')
29 | @view_config(route_name='sample_header', renderer='json')
30 | @view_config(route_name='sample_authentication', renderer='json')
31 | @view_config(route_name='sample_post', renderer='json')
32 | @view_config(route_name='post_with_form_params', renderer='json')
33 | @view_config(route_name='post_with_file_upload', renderer='json')
34 | def sample(request):
35 | if not request.registry.settings.get('skip_swagger_data_assert'):
36 | assert request.swagger_data
37 | return {}
38 |
39 |
40 | @view_config(route_name='echo_date_json_renderer', request_method='POST', renderer='json')
41 | @view_config(route_name='echo_date', request_method='POST', renderer='pyramid_swagger')
42 | def date_view(request):
43 |
44 | if '2.0' in request.registry.settings['pyramid_swagger.swagger_versions']:
45 | # Swagger 2.0 endpoint handling
46 | assert isinstance(request.swagger_data['body']['date'], datetime.date)
47 | else:
48 | assert isinstance(request.swagger_data['body']['date'], six.string_types)
49 |
50 | return request.swagger_data['body']
51 |
52 |
53 | @view_config(route_name='post_endpoint_with_optional_body', request_method='POST', renderer='pyramid_swagger')
54 | @view_config(route_name='sample_no_response_schema', request_method='GET', renderer='pyramid_swagger')
55 | def post_endpoint_with_optional_body(request):
56 | return request.content_length
57 |
58 |
59 | @view_config(route_name='swagger_undefined', renderer='json')
60 | def swagger_undefined(request):
61 | return {}
62 |
63 |
64 | def main(global_config, **settings):
65 | """ Very basic pyramid app """
66 | config = Configurator(settings=settings)
67 |
68 | config.include('pyramid_swagger')
69 |
70 | config.add_route(
71 | 'sample_nonstring',
72 | '/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}',
73 | )
74 | config.add_route('standard', '/sample/{path_arg}/resource')
75 | config.add_route('get_with_non_string_query_args', '/get_with_non_string_query_args')
76 | config.add_route('post_with_primitive_body', '/post_with_primitive_body')
77 | config.add_route('post_with_form_params', '/post_with_form_params')
78 | config.add_route('post_with_file_upload', '/post_with_file_upload')
79 | config.add_route('sample_post', '/sample')
80 | config.include(include_samples, route_prefix='/sample')
81 | config.add_route('throw_400', '/throw_400')
82 | config.add_route('swagger_undefined', '/undefined/path')
83 |
84 | config.add_route('echo_date', '/echo_date')
85 | config.add_route('echo_date_json_renderer', '/echo_date_json_renderer')
86 | config.add_route('post_endpoint_with_optional_body', '/post_endpoint_with_optional_body')
87 |
88 | config.scan()
89 | return config.make_wsgi_app()
90 |
91 |
92 | def include_samples(config):
93 | config.add_route('sample_header', '/header')
94 | config.add_route('sample_authentication', '/authentication')
95 | config.add_route('sample_no_response_schema', '/no_response_schema')
96 |
--------------------------------------------------------------------------------
/tests/includeme_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import mock
5 | import pytest
6 | from bravado_core.spec import Spec
7 | from pyramid.config import Configurator
8 | from pyramid.registry import Registry
9 | from swagger_spec_validator.common import SwaggerValidationError
10 |
11 | import pyramid_swagger
12 | from pyramid_swagger.model import SwaggerSchema
13 |
14 |
15 | @mock.patch('pyramid_swagger.register_api_doc_endpoints')
16 | @mock.patch('pyramid_swagger.get_swagger_schema')
17 | @mock.patch('pyramid_swagger.get_swagger_spec')
18 | def test_disable_api_doc_views(_1, _2, mock_register):
19 | settings = {
20 | 'pyramid_swagger.enable_api_doc_views': False,
21 | 'pyramid_swagger.enable_swagger_spec_validation': False,
22 | }
23 |
24 | mock_config = mock.Mock(
25 | spec=Configurator,
26 | registry=mock.Mock(spec=Registry, settings=settings))
27 |
28 | pyramid_swagger.includeme(mock_config)
29 | assert not mock_register.called
30 |
31 |
32 | def test_bad_schema_validated_on_include():
33 | settings = {
34 | 'pyramid_swagger.schema_directory': 'tests/sample_schemas/bad_app/',
35 | 'pyramid_swagger.enable_swagger_spec_validation': True,
36 | }
37 | mock_config = mock.Mock(registry=mock.Mock(settings=settings))
38 | with pytest.raises(SwaggerValidationError):
39 | pyramid_swagger.includeme(mock_config)
40 | # TODO: Figure out why this assertion fails on travis
41 | # assert "'info' is a required property" in str(excinfo.value)
42 |
43 |
44 | @mock.patch('pyramid_swagger.get_swagger_spec')
45 | def test_bad_schema_not_validated_if_spec_validation_is_disabled(_):
46 | settings = {
47 | 'pyramid_swagger.schema_directory': 'tests/sample_schemas/bad_app/',
48 | 'pyramid_swagger.enable_swagger_spec_validation': False,
49 | 'pyramid_swagger.enable_api_doc_views': False,
50 | }
51 | mock_config = mock.Mock(
52 | spec=Configurator, registry=mock.Mock(settings=settings))
53 | pyramid_swagger.includeme(mock_config)
54 |
55 |
56 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
57 | @mock.patch('pyramid_swagger.register_api_doc_endpoints')
58 | def test_swagger_12_only(mock_register):
59 | settings = {
60 | 'pyramid_swagger.schema_directory': 'tests/sample_schemas/good_app/',
61 | 'pyramid_swagger.swagger_versions': ['1.2']
62 | }
63 | mock_config = mock.Mock(registry=mock.Mock(settings=settings))
64 | pyramid_swagger.includeme(mock_config)
65 | assert isinstance(settings['pyramid_swagger.schema12'], SwaggerSchema)
66 | assert mock_register.call_count == 1
67 |
68 |
69 | @mock.patch('pyramid_swagger.register_api_doc_endpoints')
70 | def test_swagger_20_only(mock_register):
71 | settings = {
72 | 'pyramid_swagger.schema_directory': 'tests/sample_schemas/good_app/',
73 | 'pyramid_swagger.swagger_versions': ['2.0']
74 | }
75 | mock_config = mock.Mock(registry=mock.Mock(settings=settings))
76 | pyramid_swagger.includeme(mock_config)
77 | assert isinstance(settings['pyramid_swagger.schema20'], Spec)
78 | assert not settings['pyramid_swagger.schema12']
79 | assert mock_register.call_count == 1
80 |
81 |
82 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
83 | @mock.patch('pyramid_swagger.register_api_doc_endpoints')
84 | def test_swagger_12_and_20(mock_register):
85 | settings = {
86 | 'pyramid_swagger.schema_directory': 'tests/sample_schemas/good_app/',
87 | 'pyramid_swagger.swagger_versions': ['1.2', '2.0']
88 | }
89 | mock_config = mock.Mock(registry=mock.Mock(settings=settings))
90 | pyramid_swagger.includeme(mock_config)
91 | assert isinstance(settings['pyramid_swagger.schema20'], Spec)
92 | assert isinstance(settings['pyramid_swagger.schema12'], SwaggerSchema)
93 | assert mock_register.call_count == 2
94 |
--------------------------------------------------------------------------------
/tests/sample_schemas/good_app/other_sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "0.1",
3 | "swaggerVersion": "1.2",
4 | "basePath": "http://localhost:9999/sample",
5 | "apis": [
6 | {
7 | "path": "/sample/{path_arg}/resource",
8 | "operations": [
9 | {
10 | "method": "GET",
11 | "nickname": "standard",
12 | "type": "standard_response",
13 | "parameters": [
14 | {
15 | "paramType": "path",
16 | "name": "path_arg",
17 | "type": "string",
18 | "enum": ["path_arg1", "path_arg2"],
19 | "required": true
20 | },
21 | {
22 | "paramType": "query",
23 | "name": "required_arg",
24 | "type": "string",
25 | "required": true
26 | },
27 | {
28 | "paramType": "query",
29 | "name": "optional_arg",
30 | "type": "string",
31 | "required": false
32 | }
33 | ]
34 | }
35 | ]
36 | },
37 | {
38 | "path": "/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}",
39 | "operations": [
40 | {
41 | "method": "GET",
42 | "nickname": "sample_nonstring",
43 | "type": "void",
44 | "parameters": [
45 | {
46 | "paramType": "path",
47 | "name": "int_arg",
48 | "type": "integer",
49 | "required": true
50 | },
51 | {
52 | "paramType": "path",
53 | "name": "float_arg",
54 | "type": "number",
55 | "format": "float",
56 | "required": true
57 | },
58 | {
59 | "paramType": "path",
60 | "name": "boolean_arg",
61 | "type": "boolean",
62 | "required": true
63 | }
64 | ]
65 | }
66 | ]
67 | },
68 | {
69 | "path": "/sample/header",
70 | "operations": [
71 | {
72 | "method": "GET",
73 | "nickname": "sample_header",
74 | "type": "void",
75 | "parameters": [
76 | {
77 | "paramType": "header",
78 | "name": "X-Force",
79 | "type": "boolean",
80 | "required": true
81 | }
82 | ]
83 | }
84 | ]
85 | }
86 | ],
87 | "models": {
88 | "object": {
89 | "id": "object",
90 | "properties": { }
91 | },
92 | "standard_response": {
93 | "id": "standard_response",
94 | "type": "object",
95 | "required": [ "raw_response", "logging_info" ],
96 | "additionalProperties": false,
97 | "properties": {
98 | "raw_response": {
99 | "type": "string"
100 | },
101 | "logging_info": {
102 | "$ref": "object"
103 | }
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/tests/api_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import os
5 |
6 | import mock
7 | import pytest
8 | import yaml
9 | from bravado_core.spec import Spec
10 | from pyramid.testing import DummyRequest
11 |
12 | from pyramid_swagger.api import build_swagger_12_api_declaration_view
13 | from pyramid_swagger.api import get_path_if_relative
14 | from pyramid_swagger.api import register_api_doc_endpoints
15 | from pyramid_swagger.ingest import API_DOCS_FILENAME
16 | from pyramid_swagger.ingest import ApiDeclarationNotFoundError
17 | from pyramid_swagger.ingest import ResourceListingNotFoundError
18 | from tests.acceptance.response_test import get_registry
19 | from tests.acceptance.response_test import get_swagger_schema
20 |
21 |
22 | def test_basepath_rewriting():
23 | resource_json = {'basePath': 'bar'}
24 | view = build_swagger_12_api_declaration_view(resource_json)
25 | request = DummyRequest(application_url='foo')
26 | result = view(request)
27 | assert result['basePath'] == request.application_url
28 | assert result['basePath'] != resource_json['basePath']
29 |
30 |
31 | def build_config(schema_dir):
32 | return mock.Mock(
33 | registry=get_registry({
34 | 'swagger_schema': get_swagger_schema(schema_dir),
35 | })
36 | )
37 |
38 |
39 | def test_proper_error_on_missing_resource_listing():
40 | with pytest.raises(ResourceListingNotFoundError) as exc:
41 | register_api_doc_endpoints(
42 | build_config(
43 | 'tests/sample_schemas/missing_resource_listing/api_docs.json'),
44 | )
45 | assert(
46 | 'tests/sample_schemas/missing_resource_listing/' in str(exc.value)
47 | )
48 | assert 'must be named {0}'.format(API_DOCS_FILENAME) in str(exc.value)
49 |
50 |
51 | def test_proper_error_on_missing_api_declaration():
52 | with pytest.raises(ApiDeclarationNotFoundError) as exc:
53 | register_api_doc_endpoints(
54 | build_config('tests/sample_schemas/missing_api_declaration/'),
55 | )
56 | assert (
57 | 'tests/sample_schemas/missing_api_declaration/missing.json'
58 | in str(exc.value)
59 | )
60 |
61 |
62 | def test_ignore_absolute_paths():
63 | """
64 | we don't have the ability to automagically translate these external
65 | resources from yaml to json and vice versa, so ignore them altogether.
66 | """
67 | assert get_path_if_relative(
68 | 'http://www.google.com/some/special/schema.json',
69 | ) is None
70 |
71 | assert get_path_if_relative(
72 | '//www.google.com/some/schema.yaml',
73 | ) is None
74 |
75 | assert get_path_if_relative(
76 | '/usr/lib/shared/schema.json',
77 | ) is None
78 |
79 |
80 | def test_resolve_nested_refs():
81 | """
82 | Make sure we resolve nested refs gracefully and not get lost in
83 | the recursion. Also make sure we don't rely on dictionary order
84 | """
85 | os.environ["PYTHONHASHSEED"] = str(1)
86 | with open('tests/sample_schemas/nested_defns/swagger.yaml') as swagger_spec:
87 | spec_dict = yaml.safe_load(swagger_spec)
88 | spec = Spec.from_dict(spec_dict, '')
89 | assert spec.flattened_spec
90 |
91 |
92 | def traverse_spec(swagger_spec):
93 | for k, v in swagger_spec.items():
94 | if k == "":
95 | raise Exception('Empty key detected in the swagger spec.')
96 | elif isinstance(v, dict):
97 | return traverse_spec(v)
98 | elif isinstance(v, list):
99 | for item in v:
100 | if isinstance(item, dict):
101 | return traverse_spec(item)
102 | return
103 |
104 |
105 | def test_extenal_refs_no_empty_keys():
106 | """
107 | This test ensures that we never use empty strings as
108 | keys swagger specs.
109 | """
110 | with open('tests/sample_schemas/external_refs/swagger.json') as swagger_spec:
111 | spec_dict = yaml.safe_load(swagger_spec)
112 | path = 'file:' + os.getcwd() + '/tests/sample_schemas/external_refs/swagger.json'
113 | spec = Spec.from_dict(spec_dict, path)
114 | flattened_spec = spec.flattened_spec
115 | traverse_spec(flattened_spec)
116 |
--------------------------------------------------------------------------------
/tests/acceptance/yaml_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import base64
5 | import copy
6 | import json
7 |
8 | import pytest
9 | import yaml
10 | from bravado_core.schema import is_dict_like
11 | from bravado_core.schema import is_list_like
12 | from six import iterkeys
13 | from webtest import TestApp as App
14 |
15 | from pyramid_swagger.tween import SwaggerFormat
16 | from tests.acceptance.app import main
17 |
18 |
19 | @pytest.fixture
20 | def settings():
21 | dir_path = 'tests/sample_schemas/yaml_app/'
22 | return {
23 | 'pyramid_swagger.schema_file': 'swagger.yaml',
24 | 'pyramid_swagger.schema_directory': dir_path,
25 | 'pyramid_swagger.enable_request_validation': True,
26 | 'pyramid_swagger.enable_swagger_spec_validation': True,
27 | }
28 |
29 |
30 | @pytest.fixture
31 | def yaml_app():
32 | return SwaggerFormat(format='base64',
33 | to_wire=base64.b64encode,
34 | to_python=base64.b64decode,
35 | validate=base64.b64decode,
36 | description='base64')
37 |
38 |
39 | @pytest.fixture
40 | def testapp_with_base64(settings, yaml_app):
41 | """Fixture for setting up a Swagger 2.0 version of the test testapp."""
42 | settings['pyramid_swagger.swagger_versions'] = ['2.0']
43 | settings['pyramid_swagger.user_formats'] = [yaml_app]
44 | return App(main({}, **settings))
45 |
46 |
47 | def test_user_format_happy_case(testapp_with_base64):
48 | response = testapp_with_base64.get('/sample/path_arg1/resource',
49 | params={'required_arg': 'MQ=='},)
50 | assert response.status_code == 200
51 |
52 |
53 | def test_user_format_failure_case(testapp_with_base64):
54 | # 'MQ' is not a valid base64 encoded string.
55 | with pytest.raises(Exception):
56 | testapp_with_base64.get('/sample/path_arg1/resource',
57 | params={'required_arg': 'MQ'},)
58 |
59 |
60 | def _strip_xmodel(spec_dict):
61 | """
62 | :param spec_dict: Swagger spec in dict form. This is treated as read-only.
63 | :return: deep copy of spec_dict with the x-model vendor extension stripped out.
64 | """
65 | result = copy.deepcopy(spec_dict)
66 |
67 | def descend(fragment):
68 | if is_dict_like(fragment):
69 | fragment.pop('x-model', None) # Removes 'x-model' key if present
70 | for key in iterkeys(fragment):
71 | descend(fragment[key])
72 | elif is_list_like(fragment):
73 | for element in fragment:
74 | descend(element)
75 |
76 | descend(result)
77 | return result
78 |
79 |
80 | def validate_json_response(response, expected_dict):
81 | # webob < 1.7 returns the charset, webob >= 1.7 does not
82 | # see https://github.com/striglia/pyramid_swagger/issues/185
83 | assert response.headers['content-type'] in \
84 | ('application/json', 'application/json; charset=UTF-8')
85 | assert _strip_xmodel(json.loads(response.body.decode("utf-8"))) == expected_dict
86 |
87 |
88 | def validate_yaml_response(response, expected_dict):
89 | assert response.headers['content-type'] == 'application/x-yaml; charset=UTF-8'
90 | assert _strip_xmodel(yaml.safe_load(response.body)) == expected_dict
91 |
92 |
93 | def _rewrite_ref(ref, schema_format):
94 | if schema_format == 'yaml':
95 | return ref # all refs are already yaml
96 | return ref.replace('.yaml', '.%s' % schema_format)
97 |
98 |
99 | def _recursively_rewrite_refs(schema_item, schema_format):
100 | if isinstance(schema_item, dict):
101 | for key, value in schema_item.items():
102 | if key == '$ref':
103 | schema_item[key] = _rewrite_ref(value, schema_format)
104 | else:
105 | _recursively_rewrite_refs(value, schema_format)
106 | elif isinstance(schema_item, list):
107 | for item in schema_item:
108 | _recursively_rewrite_refs(item, schema_format)
109 |
110 |
111 | @pytest.mark.parametrize('schema_format', ['json', 'yaml'])
112 | @pytest.mark.parametrize('test_file', ['swagger', 'defs'])
113 | def test_swagger_json_api_doc_route(testapp_with_base64, test_file, schema_format):
114 | validation_method = {
115 | 'yaml': validate_yaml_response,
116 | 'json': validate_json_response,
117 | }
118 |
119 | url = '/%s.%s' % (test_file, schema_format)
120 | response = testapp_with_base64.get(url)
121 | assert response.status_code == 200
122 |
123 | fname = 'tests/sample_schemas/yaml_app/%s.yaml' % test_file
124 | with open(fname, 'r') as f:
125 | expected_schema = yaml.safe_load(f)
126 |
127 | _recursively_rewrite_refs(expected_schema, schema_format)
128 |
129 | validation_method[schema_format](response, expected_schema)
130 |
--------------------------------------------------------------------------------
/tests/acceptance/relative_ref_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import json
5 | import os.path
6 | import re
7 | import sys
8 |
9 | import pytest
10 | import yaml
11 | from six import BytesIO
12 | from webtest import TestApp as App
13 |
14 | from tests.acceptance.app import main
15 |
16 |
17 | DESERIALIZERS = {
18 | 'json': lambda r: json.loads(r.body.decode('utf-8')),
19 | 'yaml': lambda r: yaml.safe_load(BytesIO(r.body)),
20 | }
21 |
22 |
23 | @pytest.fixture
24 | def settings():
25 | dir_path = 'tests/sample_schemas/relative_ref/'
26 | return {
27 | 'pyramid_swagger.schema_directory': dir_path,
28 | 'pyramid_swagger.enable_request_validation': True,
29 | 'pyramid_swagger.enable_swagger_spec_validation': True,
30 | 'pyramid_swagger.swagger_versions': ['2.0']
31 | }
32 |
33 |
34 | @pytest.fixture
35 | def test_app(settings):
36 | """Fixture for setting up a Swagger 2.0 version of the test test_app."""
37 | return App(main({}, **settings))
38 |
39 |
40 | @pytest.fixture
41 | def test_app_deref(settings):
42 | """Fixture for setting up a Swagger 2.0 version of the test test_app
43 | test app serves swagger schemas with refs dereferenced."""
44 | settings['pyramid_swagger.dereference_served_schema'] = True
45 | return App(main({}, **settings))
46 |
47 |
48 | def test_running_query_for_relative_ref(test_app):
49 | response = test_app.get('/sample/path_arg1/resource', params={},)
50 | assert response.status_code == 200
51 |
52 |
53 | def translate_ref_extension(ref, schema_format):
54 | if schema_format == 'json':
55 | return ref # all refs are already yaml
56 | return ref.replace('.json', '.%s' % schema_format)
57 |
58 |
59 | def recursively_rewrite_refs(schema_item, schema_format):
60 | """
61 | Fix a schema's refs so that they all read the same format. Ensures that
62 | consumers requesting a yaml resource don't have to know how to read json.
63 | """
64 | if isinstance(schema_item, dict):
65 | for key, value in schema_item.items():
66 | if key == '$ref':
67 | schema_item[key] = translate_ref_extension(
68 | value, schema_format,
69 | )
70 | else:
71 | recursively_rewrite_refs(value, schema_format)
72 | elif isinstance(schema_item, list):
73 | for item in schema_item:
74 | recursively_rewrite_refs(item, schema_format)
75 |
76 |
77 | @pytest.mark.parametrize('schema_format', ['json', 'yaml', ])
78 | def test_swagger_schema_retrieval(schema_format, test_app):
79 | here = os.path.dirname(__file__)
80 | deserializer = DESERIALIZERS[schema_format]
81 |
82 | expected_files = [
83 | 'parameters/common',
84 | 'paths/common',
85 | 'responses/common',
86 | 'swagger',
87 | ]
88 | for expected_file in expected_files:
89 | response = test_app.get(
90 | '/{0}.{1}'.format(expected_file, schema_format)
91 | )
92 | assert response.status_code == 200
93 |
94 | gold_path_parts = [
95 | here,
96 | '..',
97 | 'sample_schemas',
98 | 'relative_ref',
99 | '{0}.json'.format(expected_file),
100 | ]
101 | with open(os.path.join(*gold_path_parts)) as f:
102 | expected_dict = json.load(f)
103 |
104 | recursively_rewrite_refs(expected_dict, schema_format)
105 |
106 | actual_dict = deserializer(response)
107 |
108 | assert actual_dict == expected_dict
109 |
110 |
111 | @pytest.mark.parametrize('schema_format', ['json', 'yaml', ])
112 | def test_swagger_schema_retrieval_is_not_dereferenced(schema_format, test_app):
113 |
114 | response = test_app.get('/swagger.{0}'.format(schema_format))
115 |
116 | here = os.path.dirname(__file__)
117 | swagger_path_parts = [
118 | here,
119 | '..',
120 | 'sample_schemas',
121 | 'relative_ref',
122 | 'dereferenced_swagger.json'
123 | ]
124 | dereferenced_swagger_path = os.path.join(*swagger_path_parts)
125 | with open(dereferenced_swagger_path) as swagger_file:
126 | expected_dict = json.load(swagger_file)
127 |
128 | deserializer = DESERIALIZERS[schema_format]
129 | actual_dict = deserializer(response)
130 |
131 | assert '"$ref"' in json.dumps(actual_dict)
132 | assert actual_dict != expected_dict
133 |
134 |
135 | @pytest.mark.parametrize('schema_format', ['json', 'yaml', ])
136 | def test_dereferenced_swagger_schema_retrieval(schema_format, test_app_deref):
137 |
138 | response = test_app_deref.get('/swagger.{0}'.format(schema_format))
139 |
140 | here = os.path.dirname(__file__)
141 | swagger_path_parts = [
142 | here,
143 | '..',
144 | 'sample_schemas',
145 | 'relative_ref',
146 | 'dereferenced_swagger.json'
147 | ]
148 | dereferenced_swagger_path = os.path.join(*swagger_path_parts)
149 | with open(dereferenced_swagger_path) as swagger_file:
150 | expected_dict = json.load(swagger_file)
151 |
152 | deserializer = DESERIALIZERS[schema_format]
153 | actual_dict = deserializer(response)
154 |
155 | # pattern for references outside the current file
156 | ref_pattern = re.compile(r'("\$ref": "[^#][^"]*")')
157 | assert ref_pattern.findall(json.dumps(actual_dict)) == []
158 |
159 | if sys.platform != 'win32':
160 | # This checks that the returned dictionary matches the expected one
161 | # as this check mainly validates the bravado-core performs valid flattening
162 | # of specs and bravado-core flattening could provide different results
163 | # (in terms of references names) according to the platform we decided
164 | # to not check it for windows
165 | assert actual_dict == expected_dict
166 |
--------------------------------------------------------------------------------
/tests/acceptance/api_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import os
5 |
6 | import pytest
7 | from webtest import TestApp as App
8 |
9 | from tests.acceptance.app import main
10 |
11 |
12 | @pytest.fixture
13 | def settings():
14 | return {
15 | 'pyramid_swagger.schema_directory': os.path.join('tests', 'sample_schemas', 'good_app'),
16 | 'pyramid_swagger.enable_swagger_spec_validation': False,
17 | }
18 |
19 |
20 | @pytest.fixture
21 | def default_test_app(settings):
22 | return App(main({}, **settings))
23 |
24 |
25 | @pytest.fixture
26 | def swagger_20_test_app(settings):
27 | """Fixture for setting up a Swagger 2.0 version of the test test_app."""
28 | settings['pyramid_swagger.swagger_versions'] = ['2.0']
29 | return App(main({}, **settings))
30 |
31 |
32 | @pytest.fixture
33 | def swagger_12_test_app(settings):
34 | """Fixture for setting up a Swagger 1.2 version of the test test_app."""
35 | settings['pyramid_swagger.swagger_versions'] = ['1.2']
36 | return App(main({}, **settings))
37 |
38 |
39 | @pytest.fixture
40 | def swagger_12_and_20_test_app(settings):
41 | """Fixture for setting up a Swagger 1.2 and Swagger 2.0 version of the
42 | test test_app.
43 | """
44 | settings['pyramid_swagger.swagger_versions'] = ['1.2', '2.0']
45 | return App(main({}, **settings))
46 |
47 |
48 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
49 | def test_12_api_docs(swagger_12_test_app):
50 | response = swagger_12_test_app.get('/api-docs', status=200)
51 | assert response.json['swaggerVersion'] == '1.2'
52 |
53 |
54 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
55 | def test_12_sample_resource(swagger_12_test_app):
56 | response = swagger_12_test_app.get('/api-docs/sample', status=200)
57 | assert response.json['swaggerVersion'] == '1.2'
58 |
59 |
60 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
61 | def test_12_other_sample_resource(swagger_12_test_app):
62 | response = swagger_12_test_app.get('/api-docs/other_sample', status=200)
63 | assert response.json['swaggerVersion'] == '1.2'
64 |
65 |
66 | def test_20_schema(swagger_20_test_app):
67 | response = swagger_20_test_app.get('/swagger.json', status=200)
68 | assert response.json['swagger'] == '2.0'
69 |
70 |
71 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
72 | def test_12_and_20_schemas(swagger_12_and_20_test_app):
73 | for path in ('/api-docs', '/api-docs/sample', '/api-docs/other_sample'):
74 | response12 = swagger_12_and_20_test_app.get(path, status=200)
75 | assert response12.json['swaggerVersion'] == '1.2'
76 |
77 | response20 = swagger_12_and_20_test_app.get('/swagger.json', status=200)
78 | assert response20.json['swagger'] == '2.0'
79 |
80 |
81 | def test_default_only_serves_up_swagger_20_schema(default_test_app):
82 | response = default_test_app.get('/swagger.json', status=200)
83 | assert response.json['swagger'] == '2.0'
84 |
85 | # swagger 1.2 schemas should 404
86 | for path in ('/api-docs', '/api-docs/sample', '/api-docs/other_sample'):
87 | default_test_app.get(path, status=404)
88 |
89 |
90 | def test_recursive_swagger_api_internal_refs():
91 | recursive_test_app = App(main({}, **{
92 | 'pyramid_swagger.schema_directory':
93 | 'tests/sample_schemas/recursive_app/internal/',
94 | }))
95 |
96 | recursive_test_app.get('/swagger.json', status=200)
97 |
98 |
99 | def test_recursive_swagger_api_external_refs():
100 | recursive_test_app = App(main({}, **{
101 | 'pyramid_swagger.schema_directory':
102 | 'tests/sample_schemas/recursive_app/external/',
103 | }))
104 |
105 | recursive_test_app.get('/swagger.json', status=200)
106 | recursive_test_app.get('/external.json', status=200)
107 |
108 |
109 | def test_base_path_api_docs_on_good_app_schema():
110 | base_path = '/web/base/path'
111 |
112 | recursive_test_app = App(main({}, **{
113 | 'pyramid_swagger.schema_directory':
114 | 'tests/sample_schemas/good_app/',
115 | 'pyramid_swagger.base_path_api_docs':
116 | base_path
117 | }))
118 |
119 | recursive_test_app.get(base_path + '/swagger.json', status=200)
120 | recursive_test_app.get('/swagger.json', status=404)
121 |
122 |
123 | def test_base_path_api_docs_on_recursive_app_schema():
124 | base_path = '/some/path'
125 | recursive_test_app = App(main({}, **{
126 | 'pyramid_swagger.schema_directory':
127 | 'tests/sample_schemas/recursive_app/external/',
128 | 'pyramid_swagger.base_path_api_docs':
129 | base_path
130 | }))
131 |
132 | recursive_test_app.get(base_path + '/swagger.json', status=200)
133 | recursive_test_app.get('/swagger.json', status=404)
134 | recursive_test_app.get(base_path + '/external.json', status=200)
135 | recursive_test_app.get('/external.json', status=404)
136 |
137 |
138 | def test_base_path_api_docs_with_script_name_on_recursive_app_schema():
139 | base_path = '/some/path'
140 | script_name = '/scriptname'
141 | test_app = App(main({}, **{
142 | 'pyramid_swagger.schema_directory':
143 | 'tests/sample_schemas/recursive_app/external/',
144 | 'pyramid_swagger.base_path_api_docs':
145 | base_path}),
146 | {'SCRIPT_NAME': script_name})
147 |
148 | test_app.get(script_name + base_path + '/swagger.json', status=200)
149 | test_app.get('/swagger.json', status=404)
150 |
151 | test_app.get(script_name + base_path + '/external.json', status=200)
152 | test_app.get('/external.json', status=404)
153 |
154 |
155 | def test_virtual_subpath(settings):
156 | test_app = App(main({}, **settings), {'SCRIPT_NAME': '/subpath'})
157 | test_app.get('/subpath/swagger.json', status=200)
158 |
--------------------------------------------------------------------------------
/tests/sample_schemas/good_app/sample.json:
--------------------------------------------------------------------------------
1 | {
2 | "apiVersion": "0.1",
3 | "swaggerVersion": "1.2",
4 | "basePath": "http://localhost:9999/sample",
5 | "apis": [
6 | {
7 | "path": "/sample",
8 | "operations": [
9 | {
10 | "method": "GET",
11 | "nickname": "sample_get",
12 | "type": "object",
13 | "parameters": []
14 | },
15 | {
16 | "method": "POST",
17 | "nickname": "sample_post",
18 | "type": "object",
19 | "parameters": [
20 | {
21 | "paramType": "query",
22 | "name": "optional_string",
23 | "type": "string",
24 | "required": false
25 | },
26 | {
27 | "paramType": "body",
28 | "name": "content",
29 | "type": "body_model",
30 | "required": true
31 | }
32 | ]
33 | }
34 | ]
35 | },
36 | {
37 | "path": "/sample_array_response",
38 | "operations": [
39 | {
40 | "method": "GET",
41 | "nickname": "sample_get_array_response",
42 | "parameters": [],
43 | "type": "array",
44 | "items": {
45 | "$ref": "array_content_model"
46 | }
47 | }
48 | ]
49 | },
50 | {
51 | "path": "/post_with_primitive_body",
52 | "operations": [
53 | {
54 | "method": "POST",
55 | "nickname": "post_with_primitive_body",
56 | "type": "object",
57 | "parameters": [
58 | {
59 | "paramType": "body",
60 | "name": "content",
61 | "type": "array",
62 | "items": { "type": "string" },
63 | "required": true
64 | }
65 | ]
66 | }
67 | ]
68 | },
69 | {
70 | "path": "/post_with_form_params",
71 | "operations": [
72 | {
73 | "method": "POST",
74 | "nickname": "post_with_form_params",
75 | "type": "object",
76 | "parameters": [
77 | {
78 | "name": "form_param",
79 | "type": "integer",
80 | "paramType": "form",
81 | "required": true
82 | }
83 | ]
84 | }
85 | ]
86 | },
87 | {
88 | "path": "/post_with_file_upload",
89 | "operations": [
90 | {
91 | "method": "POST",
92 | "consumes": ["multipart/form-data"],
93 | "nickname": "post_with_file_upload",
94 | "type": "object",
95 | "parameters": [
96 | {
97 | "name": "photo_file",
98 | "type": "File",
99 | "paramType": "form",
100 | "required": true
101 | }
102 | ]
103 | }
104 | ]
105 | },
106 | {
107 | "path": "/get_with_non_string_query_args",
108 | "operations": [
109 | {
110 | "method": "GET",
111 | "nickname": "get_with_non_string_query_args",
112 | "type": "object",
113 | "parameters": [
114 | {
115 | "paramType": "query",
116 | "name": "int_arg",
117 | "type": "integer",
118 | "required": true
119 | },
120 | {
121 | "paramType": "query",
122 | "name": "float_arg",
123 | "type": "number",
124 | "format": "float",
125 | "required": true
126 | },
127 | {
128 | "paramType": "query",
129 | "name": "boolean_arg",
130 | "type": "boolean",
131 | "required": true
132 | }
133 | ]
134 | }
135 | ]
136 | }
137 | ],
138 | "models": {
139 | "object": {
140 | "id": "object",
141 | "properties": { }
142 | },
143 | "body_model": {
144 | "id": "body_model",
145 | "type": "object",
146 | "required": [ "foo" ],
147 | "additionalProperties": false,
148 | "properties": {
149 | "foo": { "type": "string" },
150 | "bar": { "type": "string" }
151 | }
152 | },
153 | "standard_response": {
154 | "id": "standard_response",
155 | "type": "object",
156 | "required": [ "raw_response", "logging_info" ],
157 | "additionalProperties": false,
158 | "properties": {
159 | "raw_response": {
160 | "type": "string"
161 | },
162 | "logging_info": {
163 | "$ref": "object"
164 | }
165 | }
166 | },
167 | "array_content_model": {
168 | "id": "array_content_model",
169 | "required": ["enum_value"],
170 | "properties": {
171 | "enum_value": {
172 | "type": "string",
173 | "enum": [
174 | "good_enum_value"
175 | ]
176 | }
177 | }
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/tests/ingest_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import os.path
5 |
6 | import mock
7 | import pytest
8 | import simplejson
9 |
10 | from pyramid_swagger.ingest import _load_resource_listing
11 | from pyramid_swagger.ingest import API_DOCS_FILENAME
12 | from pyramid_swagger.ingest import ApiDeclarationNotFoundError
13 | from pyramid_swagger.ingest import BRAVADO_CORE_CONFIG_PREFIX
14 | from pyramid_swagger.ingest import create_bravado_core_config
15 | from pyramid_swagger.ingest import generate_resource_listing
16 | from pyramid_swagger.ingest import get_resource_listing
17 | from pyramid_swagger.ingest import get_swagger_schema
18 | from pyramid_swagger.ingest import get_swagger_spec
19 | from pyramid_swagger.ingest import ingest_resources
20 | from pyramid_swagger.ingest import ResourceListingGenerationError
21 | from pyramid_swagger.ingest import ResourceListingNotFoundError
22 | from pyramid_swagger.tween import SwaggerFormat
23 |
24 |
25 | def test_proper_error_on_missing_resource_listing():
26 | filename = 'tests/sample_schemas/missing_resource_listing/api_docs.json'
27 | with pytest.raises(ResourceListingNotFoundError) as exc:
28 | _load_resource_listing(filename)
29 | assert filename in str(exc.value)
30 | assert 'must be named {0}'.format(API_DOCS_FILENAME) in str(exc.value)
31 |
32 |
33 | def test_proper_error_on_missing_api_declaration():
34 | with pytest.raises(ApiDeclarationNotFoundError) as exc:
35 | ingest_resources(
36 | {'sample_resource': 'fake/sample_resource.json'},
37 | 'fake',
38 | )
39 | assert 'fake/sample_resource.json' in str(exc.value)
40 |
41 |
42 | @mock.patch('pyramid_swagger.ingest.build_http_handlers',
43 | return_value={'file': mock.Mock()})
44 | @mock.patch('os.path.abspath', return_value='/bar/foo/swagger.json')
45 | @mock.patch('pyramid_swagger.ingest.Spec.from_dict')
46 | def test_get_swagger_spec_passes_absolute_url(
47 | mock_spec, mock_abs, mock_http_handlers,
48 | ):
49 | get_swagger_spec({'pyramid_swagger.schema_directory': 'foo/'})
50 | mock_abs.assert_called_once_with('foo/swagger.json')
51 | expected_url = "file:///bar/foo/swagger.json"
52 | mock_spec.assert_called_once_with(mock.ANY, config=mock.ANY,
53 | origin_url=expected_url)
54 |
55 |
56 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
57 | def test_get_swagger_schema_default():
58 | settings = {
59 | 'pyramid_swagger.schema_directory': 'tests/sample_schemas/good_app/',
60 | }
61 |
62 | swagger_schema = get_swagger_schema(settings)
63 | assert len(swagger_schema.pyramid_endpoints) == 6
64 | assert swagger_schema.resource_validators
65 |
66 |
67 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
68 | def test_get_swagger_schema_no_validation():
69 | settings = {
70 | 'pyramid_swagger.schema_directory': 'tests/sample_schemas/bad_app/',
71 | 'pyramid_swagger.enable_swagger_spec_validation': False,
72 | }
73 | # No error means we skipped validation of the bad schema
74 | get_swagger_schema(settings)
75 |
76 |
77 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
78 | def test_generate_resource_listing():
79 | listing = {'swaggerVersion': 1.2}
80 |
81 | listing = generate_resource_listing(
82 | 'tests/sample_schemas/good_app/',
83 | listing
84 | )
85 |
86 | expected = {
87 | 'swaggerVersion': 1.2,
88 | 'apis': [
89 | {'path': '/echo_date'},
90 | {'path': '/no_models'},
91 | {'path': '/other_sample'},
92 | {'path': '/post_endpoint_with_optional_body'},
93 | {'path': '/sample'},
94 | ]
95 | }
96 | assert listing == expected
97 |
98 |
99 | def test_generate_resource_listing_with_existing_listing():
100 | listing = {
101 | 'apis': [{'path': '/something'}]
102 | }
103 | with pytest.raises(ResourceListingGenerationError) as exc:
104 | generate_resource_listing('tests/sample_schemas/good_app/', listing)
105 |
106 | assert 'Generating a listing would override' in str(exc.value)
107 |
108 |
109 | @mock.patch('pyramid_swagger.ingest.generate_resource_listing', autospec=True)
110 | @mock.patch('pyramid_swagger.ingest._load_resource_listing', autospec=True)
111 | def test_get_resource_listing_generated(mock_load, mock_generate):
112 | schema_dir = '/api_docs'
113 | resource_listing = get_resource_listing(schema_dir, True)
114 | mock_generate.assert_called_once_with(schema_dir, mock_load.return_value)
115 | assert resource_listing == mock_generate.return_value
116 |
117 |
118 | def test_get_resource_listing_default():
119 | schema_dir = 'tests/sample_schemas/good_app/'
120 | resource_listing = get_resource_listing(schema_dir, False)
121 |
122 | with open(os.path.join(schema_dir, 'api_docs.json')) as fh:
123 | assert resource_listing == simplejson.load(fh)
124 |
125 |
126 | def test_create_bravado_core_config_with_defaults():
127 | assert {'use_models': False} == create_bravado_core_config({})
128 |
129 |
130 | @pytest.fixture
131 | def bravado_core_formats():
132 | return [mock.Mock(spec=SwaggerFormat)]
133 |
134 |
135 | @pytest.fixture
136 | def bravado_core_configs(bravado_core_formats):
137 | return {
138 | 'validate_requests': True,
139 | 'validate_responses': False,
140 | 'validate_swagger_spec': True,
141 | 'use_models': True,
142 | 'formats': bravado_core_formats,
143 | 'include_missing_properties': False
144 | }
145 |
146 |
147 | def test_create_bravado_core_config_non_empty_deprecated_configs(
148 | bravado_core_formats, bravado_core_configs,
149 | ):
150 | pyramid_swagger_config = {
151 | 'pyramid_swagger.enable_request_validation': True,
152 | 'pyramid_swagger.enable_response_validation': False,
153 | 'pyramid_swagger.enable_swagger_spec_validation': True,
154 | 'pyramid_swagger.use_models': True,
155 | 'pyramid_swagger.user_formats': bravado_core_formats,
156 | 'pyramid_swagger.include_missing_properties': False,
157 | }
158 |
159 | bravado_core_config = create_bravado_core_config(pyramid_swagger_config)
160 |
161 | assert bravado_core_configs == bravado_core_config
162 |
163 |
164 | def test_create_bravado_core_config_with_passthrough_configs(bravado_core_formats, bravado_core_configs):
165 | pyramid_swagger_config = {
166 | '{}validate_requests'.format(BRAVADO_CORE_CONFIG_PREFIX): True,
167 | '{}validate_responses'.format(BRAVADO_CORE_CONFIG_PREFIX): False,
168 | '{}validate_swagger_spec'.format(BRAVADO_CORE_CONFIG_PREFIX): True,
169 | '{}use_models'.format(BRAVADO_CORE_CONFIG_PREFIX): True,
170 | '{}formats'.format(BRAVADO_CORE_CONFIG_PREFIX): bravado_core_formats,
171 | '{}include_missing_properties'.format(BRAVADO_CORE_CONFIG_PREFIX): False
172 | }
173 |
174 | bravado_core_config = create_bravado_core_config(pyramid_swagger_config)
175 |
176 | assert bravado_core_configs == bravado_core_config
177 |
--------------------------------------------------------------------------------
/tests/sample_schemas/bad_app/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "paths": {
4 | "/no_models": {
5 | "get": {
6 | "responses": {
7 | "200": {
8 | "description": "No response was specified"
9 | }
10 | },
11 | "description": "",
12 | "operationId": "no_models_get"
13 | }
14 | },
15 | "/sample/{path_arg}/resource": {
16 | "get": {
17 | "responses": {
18 | "200": {
19 | "description": "No response was specified"
20 | }
21 | },
22 | "description": "",
23 | "operationId": "standard",
24 | "parameters": [
25 | {
26 | "in": "path",
27 | "name": "path_arg",
28 | "required": true,
29 | "type": "string",
30 | "enum": ["path_arg1", "path_arg2"]
31 | },
32 | {
33 | "in": "query",
34 | "name": "required_arg",
35 | "required": true,
36 | "type": "string"
37 | },
38 | {
39 | "in": "query",
40 | "name": "optional_arg",
41 | "required": false,
42 | "type": "string"
43 | }
44 | ]
45 | }
46 | },
47 | "/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}": {
48 | "get": {
49 | "responses": {
50 | "200": {
51 | "description": "No response was specified"
52 | }
53 | },
54 | "description": "",
55 | "operationId": "sample_nonstring",
56 | "parameters": [
57 | {
58 | "in": "path",
59 | "name": "int_arg",
60 | "required": true,
61 | "type": "integer"
62 | },
63 | {
64 | "in": "path",
65 | "name": "float_arg",
66 | "required": true,
67 | "type": "number"
68 | },
69 | {
70 | "in": "path",
71 | "name": "boolean_arg",
72 | "required": true,
73 | "type": "boolean"
74 | }
75 | ]
76 | }
77 | },
78 | "/sample/header": {
79 | "get": {
80 | "responses": {
81 | "200": {
82 | "description": "No response was specified"
83 | }
84 | },
85 | "description": "",
86 | "operationId": "sample_header",
87 | "parameters": [
88 | {
89 | "in": "header",
90 | "name": "X-Force",
91 | "required": true,
92 | "type": "boolean"
93 | }
94 | ]
95 | }
96 | },
97 | "/sample": {
98 | "get": {
99 | "responses": {
100 | "200": {
101 | "description": "No response was specified"
102 | }
103 | },
104 | "description": "",
105 | "operationId": "sample_get"
106 | },
107 | "post": {
108 | "responses": {
109 | "200": {
110 | "description": "No response was specified"
111 | }
112 | },
113 | "description": "",
114 | "operationId": "sample_post",
115 | "parameters": [
116 | {
117 | "in": "query",
118 | "name": "optional_string",
119 | "required": false,
120 | "type": "string"
121 | },
122 | {
123 | "in": "body",
124 | "name": "content",
125 | "required": true,
126 | "schema": {
127 | "$ref": "#/definitions/body_model"
128 | }
129 | }
130 | ]
131 | }
132 | },
133 | "/sample_array_response": {
134 | "get": {
135 | "responses": {
136 | "200": {
137 | "description": "No response was specified"
138 | }
139 | },
140 | "description": "",
141 | "operationId": "sample_get_array_response"
142 | }
143 | },
144 | "/post_with_primitive_body": {
145 | "post": {
146 | "responses": {
147 | "200": {
148 | "description": "No response was specified"
149 | }
150 | },
151 | "description": "",
152 | "operationId": "post_with_primitive_body",
153 | "parameters": [
154 | {
155 | "in": "body",
156 | "name": "content",
157 | "required": true,
158 | "schema": {
159 | "type": "array",
160 | "items": {
161 | "type": "string"
162 | }
163 | }
164 | }
165 | ]
166 | }
167 | },
168 | "/post_with_form_params": {
169 | "post": {
170 | "200": {
171 | "description": "No response was specified"
172 | },
173 | "description": "",
174 | "operationId": "post_with_form_params",
175 | "parameters": [
176 | {
177 | "in": "formData",
178 | "name": "form_param",
179 | "required": true,
180 | "type": "integer"
181 | }
182 | ]
183 | }
184 | },
185 | "/get_with_non_string_query_args": {
186 | "get": {
187 | "responses": {
188 | "200": {
189 | "description": "No response was specified"
190 | }
191 | },
192 | "description": "",
193 | "operationId": "get_with_non_string_query_args",
194 | "parameters": [
195 | {
196 | "in": "query",
197 | "name": "int_arg",
198 | "required": true,
199 | "type": "integer"
200 | },
201 | {
202 | "in": "query",
203 | "name": "float_arg",
204 | "required": true,
205 | "type": "number"
206 | },
207 | {
208 | "in": "query",
209 | "name": "boolean_arg",
210 | "required": true,
211 | "type": "boolean"
212 | }
213 | ]
214 | }
215 | }
216 | },
217 | "host": "localhost:9999",
218 | "basePath": "/sample",
219 | "schemes": [
220 | "http"
221 | ],
222 | "definitions": {
223 | "standard_response": {
224 | "type": "object",
225 | "required": [
226 | "raw_response",
227 | "logging_info"
228 | ],
229 | "additionalProperties": false,
230 | "properties": {
231 | "raw_response": {
232 | "type": "string"
233 | },
234 | "logging_info": {
235 | "type": "object"
236 | }
237 | }
238 | },
239 | "body_model": {
240 | "type": "object",
241 | "required": [
242 | "foo"
243 | ],
244 | "additionalProperties": false,
245 | "properties": {
246 | "foo": {
247 | "type": "string"
248 | },
249 | "bar": {
250 | "type": "string"
251 | }
252 | }
253 | },
254 | "array_content_model": {
255 | "required": [
256 | "enum_value"
257 | ],
258 | "properties": {
259 | "enum_value": {
260 | "type": "string",
261 | "enum": [
262 | "good_enum_value"
263 | ]
264 | }
265 | }
266 | }
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # pyramid_swagger documentation build configuration file, created by
4 | # sphinx-quickstart on Mon May 12 13:42:31 2014.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 | from __future__ import absolute_import
14 |
15 | import os
16 |
17 |
18 | # If extensions (or modules to document with autodoc) are in another directory,
19 | # add these directories to sys.path here. If the directory is relative to the
20 | # documentation root, use os.path.abspath to make it absolute, like shown here.
21 | # sys.path.append(os.path.abspath('.'))
22 |
23 | # -- General configuration -----------------------------------------------------
24 |
25 | # Add any Sphinx extension module names here, as strings. They can be extensions
26 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
27 | extensions = []
28 |
29 | # Add any paths that contain templates here, relative to this directory.
30 | templates_path = ['_templates']
31 |
32 | # The suffix of source filenames.
33 | source_suffix = '.rst'
34 |
35 | # The encoding of source files.
36 | #source_encoding = 'utf-8'
37 |
38 | # The master toctree document.
39 | master_doc = 'index'
40 |
41 | # General information about the project.
42 | project = u'pyramid_swagger'
43 | copyright = u'2014, Scott Triglia'
44 |
45 | # The version info for the project you're documenting, acts as replacement for
46 | # |version| and |release|, also used in various other places throughout the
47 | # built documents.
48 | #
49 | # The short X.Y version.
50 | version = '0.1.0'
51 | # The full version, including alpha/beta/rc tags.
52 | release = '0.1.0'
53 |
54 | # The language for content autogenerated by Sphinx. Refer to documentation
55 | # for a list of supported languages.
56 | #language = None
57 |
58 | # There are two options for replacing |today|: either, you set today to some
59 | # non-false value, then it is used:
60 | #today = ''
61 | # Else, today_fmt is used as the format for a strftime call.
62 | #today_fmt = '%B %d, %Y'
63 |
64 | # List of documents that shouldn't be included in the build.
65 | #unused_docs = []
66 |
67 | # List of directories, relative to source directory, that shouldn't be searched
68 | # for source files.
69 | exclude_trees = []
70 |
71 | # The reST default role (used for this markup: `text`) to use for all documents.
72 | #default_role = None
73 |
74 | # If true, '()' will be appended to :func: etc. cross-reference text.
75 | #add_function_parentheses = True
76 |
77 | # If true, the current module name will be prepended to all description
78 | # unit titles (such as .. function::).
79 | #add_module_names = True
80 |
81 | # If true, sectionauthor and moduleauthor directives will be shown in the
82 | # output. They are ignored by default.
83 | #show_authors = False
84 |
85 | # The name of the Pygments (syntax highlighting) style to use.
86 | pygments_style = 'sphinx'
87 |
88 | # A list of ignored prefixes for module index sorting.
89 | #modindex_common_prefix = []
90 |
91 |
92 | # -- Options for HTML output ---------------------------------------------------
93 |
94 | # The theme to use for HTML and HTML Help pages. Major themes that come with
95 | # Sphinx are currently 'default' and 'sphinxdoc'.
96 | html_theme = 'sphinx_rtd_theme'
97 |
98 | # Theme options are theme-specific and customize the look and feel of a theme
99 | # further. For a list of options available for each theme, see the
100 | # documentation.
101 | #html_theme_options = {}
102 |
103 | # Add any paths that contain custom themes here, relative to this directory.
104 | #html_theme_path = []
105 |
106 | # The name for this set of Sphinx documents. If None, it defaults to
107 | # " v documentation".
108 | #html_title = None
109 |
110 | # A shorter title for the navigation bar. Default is the same as html_title.
111 | #html_short_title = None
112 |
113 | # The name of an image file (relative to this directory) to place at the top
114 | # of the sidebar.
115 | #html_logo = None
116 |
117 | # The name of an image file (within the static path) to use as favicon of the
118 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
119 | # pixels large.
120 | #html_favicon = None
121 |
122 | # Add any paths that contain custom static files (such as style sheets) here,
123 | # relative to this directory. They are copied after the builtin static files,
124 | # so a file named "default.css" will overwrite the builtin "default.css".
125 | #html_static_path = ['_static']
126 |
127 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
128 | # using the given strftime format.
129 | #html_last_updated_fmt = '%b %d, %Y'
130 |
131 | # If true, SmartyPants will be used to convert quotes and dashes to
132 | # typographically correct entities.
133 | #html_use_smartypants = True
134 |
135 | # Custom sidebar templates, maps document names to template names.
136 | #html_sidebars = {}
137 |
138 | # Additional templates that should be rendered to pages, maps page names to
139 | # template names.
140 | #html_additional_pages = {}
141 |
142 | # If false, no module index is generated.
143 | #html_use_modindex = True
144 |
145 | # If false, no index is generated.
146 | #html_use_index = True
147 |
148 | # If true, the index is split into individual pages for each letter.
149 | #html_split_index = False
150 |
151 | # If true, links to the reST sources are added to the pages.
152 | #html_show_sourcelink = True
153 |
154 | # If true, an OpenSearch description file will be output, and all pages will
155 | # contain a tag referring to it. The value of this option must be the
156 | # base URL from which the finished HTML is served.
157 | #html_use_opensearch = ''
158 |
159 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
160 | #html_file_suffix = ''
161 |
162 | # Output file base name for HTML help builder.
163 | htmlhelp_basename = 'pyramid_swaggerdoc'
164 |
165 |
166 | # -- Options for LaTeX output --------------------------------------------------
167 |
168 | # The paper size ('letter' or 'a4').
169 | #latex_paper_size = 'letter'
170 |
171 | # The font size ('10pt', '11pt' or '12pt').
172 | #latex_font_size = '10pt'
173 |
174 | # Grouping the document tree into LaTeX files. List of tuples
175 | # (source start file, target name, title, author, documentclass [howto/manual]).
176 | latex_documents = [
177 | ('index', 'pyramid_swagger.tex', u'pyramid\\_swagger Documentation',
178 | u'Scott Triglia', 'manual'),
179 | ]
180 | latex_elements = {
181 | # Additional stuff for the LaTeX preamble. See
182 | # https://github.com/rtfd/readthedocs.org/issues/416
183 | 'preamble': "".join((
184 | '\DeclareUnicodeCharacter{00A0}{ }', # NO-BREAK SPACE
185 | '\DeclareUnicodeCharacter{251C}{+}', # BOX DRAWINGS LIGHT VERTICAL AND RIGHT
186 | '\DeclareUnicodeCharacter{2514}{+}', # BOX DRAWINGS LIGHT UP AND RIGHT
187 | )),
188 | }
189 |
190 | # The name of an image file (relative to this directory) to place at the top of
191 | # the title page.
192 | #latex_logo = None
193 |
194 | # For "manual" documents, if this is true, then toplevel headings are parts,
195 | # not chapters.
196 | #latex_use_parts = False
197 |
198 | # Additional stuff for the LaTeX preamble.
199 | #latex_preamble = ''
200 |
201 | # Documents to append as an appendix to all manuals.
202 | #latex_appendices = []
203 |
204 | # If false, no module index is generated.
205 | #latex_use_modindex = True
206 |
--------------------------------------------------------------------------------
/tests/acceptance/response20_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Swagger 2.0 response acceptance tests
4 | #
5 | # Based on request_test.py (Swagger 1.2 tests). Differences made it hard for
6 | # a single codebase to exercise both Swagger 1.2 and 2.0 responses.
7 | #
8 | from __future__ import absolute_import
9 |
10 | import pytest
11 | import simplejson
12 | from mock import Mock
13 | from mock import patch
14 | from pyramid.interfaces import IRoute
15 | from pyramid.interfaces import IRoutesMapper
16 | from pyramid.response import Response
17 | from webtest.app import AppError
18 |
19 | from pyramid_swagger.exceptions import ResponseValidationError
20 | from pyramid_swagger.ingest import get_swagger_spec
21 | from pyramid_swagger.tween import validation_tween_factory
22 | from tests.acceptance.request_test import build_test_app
23 | from tests.acceptance.response_test import CustomResponseValidationException
24 | from tests.acceptance.response_test import EnhancedDummyRequest
25 | from tests.acceptance.response_test import get_registry
26 | from tests.acceptance.response_test import validation_ctx_path
27 |
28 |
29 | def _validate_against_tween(request, response=None, path_pattern='/',
30 | **overrides):
31 | """
32 | Acceptance testing helper for testing the swagger tween with Swagger 2.0
33 | responses.
34 |
35 | :param request: pyramid request
36 | :param response: standard fixture by default
37 | :param path_pattern: Path pattern eg. /foo/{bar}
38 | :param overrides: dict of overrides for `pyramid_swagger` config
39 | """
40 | def handler(request):
41 | return response or Response()
42 |
43 | settings = dict({
44 | 'pyramid_swagger.schema_directory': 'tests/sample_schemas/good_app/',
45 | 'pyramid_swagger.enable_swagger_spec_validation': False,
46 | 'pyramid_swagger.enable_response_validation': True,
47 | 'pyramid_swagger.swagger_versions': ['2.0']},
48 | **overrides
49 | )
50 |
51 | spec = get_swagger_spec(settings)
52 | settings['pyramid_swagger.schema12'] = None
53 | settings['pyramid_swagger.schema20'] = spec
54 |
55 | registry = get_registry(settings)
56 |
57 | # This is a little messy because the current flow of execution doesn't
58 | # set up the route_info in pyramid. Have to mock out the `route_info`
59 | # so that usages in the tween meet expectations. Holler if you know a
60 | # better way to do this!
61 | op = spec.get_op_for_request(request.method, path_pattern)
62 | mock_route = Mock(spec=IRoute)
63 | mock_route.name = path_pattern
64 | mock_route_info = {'match': request.matchdict, 'route': mock_route}
65 | mock_route_mapper = Mock(spec=IRoutesMapper, return_value=mock_route_info)
66 | with patch('pyramid_swagger.tween.get_op_for_request', return_value=op):
67 | with patch('pyramid.registry.Registry.queryUtility',
68 | return_value=mock_route_mapper):
69 | validation_tween_factory(handler, registry)(request)
70 |
71 |
72 | def test_response_validation_enabled_by_default():
73 | request = EnhancedDummyRequest(
74 | method='GET',
75 | path='/sample/path_arg1/resource',
76 | params={'required_arg': 'test'},
77 | matchdict={'path_arg': 'path_arg1'},
78 | )
79 | # Omit the logging_info key from the response. If response validation
80 | # occurs, we'll fail it.
81 | response = Response(
82 | body=simplejson.dumps({'raw_response': 'foo'}),
83 | headers={'Content-Type': 'application/json; charset=UTF-8'},
84 | )
85 | with pytest.raises(ResponseValidationError):
86 | _validate_against_tween(
87 | request,
88 | response=response,
89 | path_pattern='/sample/{path_arg}/resource')
90 |
91 |
92 | def test_500_when_response_is_missing_required_field():
93 |
94 | request = EnhancedDummyRequest(
95 | method='GET',
96 | path='/sample/path_arg1/resource',
97 | params={'required_arg': 'test'},
98 | matchdict={'path_arg': 'path_arg1'},
99 | )
100 |
101 | # Omit the logging_info key from the response to induce failure
102 | response = Response(
103 | body=simplejson.dumps({'raw_response': 'foo'}),
104 | headers={'Content-Type': 'application/json; charset=UTF-8'},
105 | )
106 |
107 | with pytest.raises(ResponseValidationError) as excinfo:
108 | _validate_against_tween(
109 | request,
110 | response=response,
111 | path_pattern='/sample/{path_arg}/resource')
112 |
113 | assert "'logging_info' is a required property" in str(excinfo.value)
114 |
115 |
116 | def test_200_when_response_is_void_with_none_response():
117 | request = EnhancedDummyRequest(
118 | method='GET',
119 | path='/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}',
120 | params={'required_arg': 'test'},
121 | matchdict={'int_arg': '1', 'float_arg': '2.0', 'boolean_arg': 'true'},
122 | )
123 | response = Response(
124 | body=simplejson.dumps(None),
125 | headers={'Content-Type': 'application/json; charset=UTF-8'},
126 | )
127 | _validate_against_tween(
128 | request,
129 | response=response,
130 | path_pattern='/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}')
131 |
132 |
133 | def test_200_when_response_is_void_with_empty_response():
134 | request = EnhancedDummyRequest(
135 | method='GET',
136 | path='/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}',
137 | params={'required_arg': 'test'},
138 | matchdict={'int_arg': '1', 'float_arg': '2.0', 'boolean_arg': 'true'},
139 | )
140 | response = Response(body='{}')
141 | _validate_against_tween(
142 | request,
143 | response=response,
144 | path_pattern='/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}')
145 |
146 |
147 | def test_500_when_response_arg_is_wrong_type():
148 | request = EnhancedDummyRequest(
149 | method='GET',
150 | path='/sample/path_arg1/resource',
151 | params={'required_arg': 'test'},
152 | matchdict={'path_arg': 'path_arg1'},
153 | )
154 | response = Response(
155 | body=simplejson.dumps({
156 | 'raw_response': 1.0, # <-- is supposed to be a string
157 | 'logging_info': {'foo': 'bar'}
158 | }),
159 | headers={'Content-Type': 'application/json; charset=UTF-8'},
160 | )
161 | with pytest.raises(ResponseValidationError) as excinfo:
162 | _validate_against_tween(
163 | request,
164 | response=response,
165 | path_pattern='/sample/{path_arg}/resource')
166 |
167 | assert "1.0 is not of type " in str(excinfo.value)
168 |
169 |
170 | def test_500_for_bad_validated_array_response():
171 | request = EnhancedDummyRequest(
172 | method='GET',
173 | path='/sample_array_response',
174 | )
175 | response = Response(
176 | body=simplejson.dumps([{"enum_value": "bad_enum_value"}]),
177 | headers={'Content-Type': 'application/json; charset=UTF-8'},
178 | )
179 | with pytest.raises(ResponseValidationError) as excinfo:
180 | _validate_against_tween(
181 | request,
182 | response=response,
183 | path_pattern='/sample_array_response')
184 |
185 | assert "'bad_enum_value' is not one of " in \
186 | str(excinfo.value)
187 |
188 |
189 | def test_response_not_validated_if_route_in_response_validation_exclude_routes():
190 | request = EnhancedDummyRequest(
191 | method='GET',
192 | path='/sample_array_response',
193 | )
194 | response = Response(
195 | body='{}',
196 | headers={'Content-Type': 'application/json; charset=UTF-8'},
197 | )
198 |
199 | _validate_against_tween(
200 | request,
201 | response=response,
202 | path_pattern='/sample_array_response',
203 | # the route name is configured to be the same as the path_pattern in `_validate_against_tween`
204 | **{'pyramid_swagger.response_validation_exclude_routes': {'/sample_array_response'}})
205 |
206 |
207 | def test_200_for_good_validated_array_response():
208 | request = EnhancedDummyRequest(
209 | method='GET',
210 | path='/sample_array_response',
211 | )
212 | response = Response(
213 | body=simplejson.dumps([{"enum_value": "good_enum_value"}]),
214 | headers={'Content-Type': 'application/json; charset=UTF-8'},
215 | )
216 |
217 | _validate_against_tween(
218 | request,
219 | response=response,
220 | path_pattern='/sample_array_response')
221 |
222 |
223 | def test_200_for_normal_response_validation():
224 | assert build_test_app(
225 | swagger_versions=['2.0'],
226 | **{'pyramid_swagger.enable_response_validation': True}
227 | ).post_json(
228 | '/sample', {'foo': 'test', 'bar': 'test'},
229 | ).status_code == 200
230 |
231 |
232 | def test_app_error_if_path_not_in_spec_and_path_validation_disabled():
233 | """If path missing and validation is disabled we want to let something else
234 | handle the error. TestApp throws an AppError, but Pyramid would throw a
235 | HTTPNotFound exception.
236 | """
237 | with pytest.raises(AppError):
238 | assert build_test_app(
239 | swagger_versions=['2.0'],
240 | **{'pyramid_swagger.enable_path_validation': False}
241 | ).get('/this/path/doesnt/exist')
242 |
243 |
244 | def test_response_validation_context():
245 | request = EnhancedDummyRequest(
246 | method='GET',
247 | path='/sample/path_arg1/resource',
248 | params={'required_arg': 'test'},
249 | matchdict={'path_arg': 'path_arg1'},
250 | )
251 | # Omit the logging_info key from the response.
252 | response = Response(
253 | body=simplejson.dumps({'raw_response': 'foo'}),
254 | headers={'Content-Type': 'application/json; charset=UTF-8'},
255 | )
256 |
257 | with pytest.raises(CustomResponseValidationException):
258 | _validate_against_tween(
259 | request,
260 | response=response,
261 | path_pattern='/sample/{path_arg}/resource',
262 | **{'pyramid_swagger.validation_context_path': validation_ctx_path}
263 | )
264 |
--------------------------------------------------------------------------------
/pyramid_swagger/ingest.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 | from __future__ import unicode_literals
4 |
5 | import glob
6 | import os.path
7 |
8 | import simplejson
9 | from bravado_core.spec import build_http_handlers
10 | from bravado_core.spec import Spec
11 | from six import iteritems
12 | from six.moves.urllib import parse as urlparse
13 | from six.moves.urllib.request import pathname2url
14 |
15 | from pyramid_swagger.api import build_swagger_12_endpoints
16 | from pyramid_swagger.load_schema import load_schema
17 | from pyramid_swagger.model import SwaggerSchema
18 | from pyramid_swagger.spec import API_DOCS_FILENAME
19 | from pyramid_swagger.spec import validate_swagger_schema
20 |
21 |
22 | # Prefix of configs that will be passed to the underlying bravado-core instance
23 | BRAVADO_CORE_CONFIG_PREFIX = 'bravado_core.'
24 |
25 |
26 | class ResourceListingNotFoundError(Exception):
27 | pass
28 |
29 |
30 | class ApiDeclarationNotFoundError(Exception):
31 | pass
32 |
33 |
34 | class ResourceListingGenerationError(Exception):
35 | pass
36 |
37 |
38 | def find_resource_names(api_docs_json):
39 | return [api['path'].lstrip('/') for api in api_docs_json['apis']]
40 |
41 |
42 | def find_resource_paths(schema_dir):
43 | """The inverse of :func:`find_resource_names` used to generate
44 | a resource listing from a directory of swagger api docs.
45 | """
46 | def not_api_doc_file(filename):
47 | return not filename.endswith(API_DOCS_FILENAME)
48 |
49 | def not_swagger_dot_json(filename):
50 | # Exclude a Swagger 2.0 schema file if it happens to exist.
51 | return not os.path.basename(filename) == 'swagger.json'
52 |
53 | def filename_to_path(filename):
54 | filename, _ext = os.path.splitext(os.path.basename(filename))
55 | return '/' + filename
56 |
57 | filenames = glob.glob('{0}/*.json'.format(schema_dir))
58 | return map(filename_to_path,
59 | filter(not_swagger_dot_json,
60 | filter(not_api_doc_file, sorted(filenames))))
61 |
62 |
63 | def build_schema_mapping(schema_dir, resource_listing):
64 | """Discovers schema file locations and relations.
65 |
66 | :param schema_dir: the directory schema files live inside
67 | :type schema_dir: string
68 | :param resource_listing: A swagger resource listing
69 | :type resource_listing: dict
70 | :returns: a mapping from resource name to file path
71 | :rtype: dict
72 | """
73 | def resource_name_to_filepath(name):
74 | return os.path.join(schema_dir, '{0}.json'.format(name))
75 |
76 | return dict(
77 | (resource, resource_name_to_filepath(resource))
78 | for resource in find_resource_names(resource_listing)
79 | )
80 |
81 |
82 | def _load_resource_listing(resource_listing):
83 | """Load the resource listing from file, handling errors.
84 |
85 | :param resource_listing: path to the api-docs resource listing file
86 | :type resource_listing: string
87 | :returns: contents of the resource listing file
88 | :rtype: dict
89 | """
90 | try:
91 | with open(resource_listing) as resource_listing_file:
92 | return simplejson.load(resource_listing_file)
93 | # If not found, raise a more user-friendly error.
94 | except IOError:
95 | raise ResourceListingNotFoundError(
96 | 'No resource listing found at {0}. Note that your json file '
97 | 'must be named {1}'.format(resource_listing, API_DOCS_FILENAME)
98 | )
99 |
100 |
101 | def generate_resource_listing(schema_dir, listing_base):
102 | if 'apis' in listing_base:
103 | raise ResourceListingGenerationError(
104 | "{0}/{1} has an `apis` listing. Generating a listing would "
105 | "override this listing.".format(schema_dir, API_DOCS_FILENAME))
106 |
107 | return dict(
108 | listing_base,
109 | apis=[{'path': path} for path in find_resource_paths(schema_dir)]
110 | )
111 |
112 |
113 | def get_resource_listing(schema_dir, should_generate_resource_listing):
114 | """Return the resource listing document.
115 |
116 | :param schema_dir: the directory which contains swagger spec files
117 | :type schema_dir: string
118 | :param should_generate_resource_listing: when True a resource listing will
119 | be generated from the list of *.json files in the schema_dir. Otherwise
120 | return the contents of the resource listing file
121 | :type should_generate_resource_listing: boolean
122 | :returns: the contents of a resource listing document
123 | """
124 | listing_filename = os.path.join(schema_dir, API_DOCS_FILENAME)
125 | resource_listing = _load_resource_listing(listing_filename)
126 |
127 | if not should_generate_resource_listing:
128 | return resource_listing
129 | return generate_resource_listing(schema_dir, resource_listing)
130 |
131 |
132 | def compile_swagger_schema(schema_dir, resource_listing):
133 | """Build a SwaggerSchema from various files.
134 |
135 | :param schema_dir: the directory schema files live inside
136 | :type schema_dir: string
137 | :returns: a SwaggerSchema object
138 | """
139 | mapping = build_schema_mapping(schema_dir, resource_listing)
140 | resource_validators = ingest_resources(mapping, schema_dir)
141 | endpoints = list(build_swagger_12_endpoints(resource_listing, mapping))
142 | return SwaggerSchema(endpoints, resource_validators)
143 |
144 |
145 | def get_swagger_schema(settings):
146 | """Return a :class:`pyramid_swagger.model.SwaggerSchema` constructed from
147 | the swagger specs in `pyramid_swagger.schema_directory`. If
148 | `pyramid_swagger.enable_swagger_spec_validation` is enabled the schema
149 | will be validated before returning it.
150 |
151 | :param settings: a pyramid registry settings with configuration for
152 | building a swagger schema
153 | :type settings: dict
154 | :returns: a :class:`pyramid_swagger.model.SwaggerSchema`
155 | """
156 | schema_dir = settings.get('pyramid_swagger.schema_directory', 'api_docs')
157 | resource_listing = get_resource_listing(
158 | schema_dir,
159 | settings.get('pyramid_swagger.generate_resource_listing', False)
160 | )
161 |
162 | if settings.get('pyramid_swagger.enable_swagger_spec_validation', True):
163 | validate_swagger_schema(schema_dir, resource_listing)
164 |
165 | return compile_swagger_schema(schema_dir, resource_listing)
166 |
167 |
168 | def get_swagger_spec(settings):
169 | """Return a :class:`bravado_core.spec.Spec` constructed from
170 | the swagger specs in `pyramid_swagger.schema_directory`. If
171 | `pyramid_swagger.enable_swagger_spec_validation` is enabled the schema
172 | will be validated before returning it.
173 |
174 | :param settings: a pyramid registry settings with configuration for
175 | building a swagger schema
176 | :type settings: dict
177 | :rtype: :class:`bravado_core.spec.Spec`
178 | """
179 | schema_dir = settings.get('pyramid_swagger.schema_directory', 'api_docs/')
180 | schema_filename = settings.get('pyramid_swagger.schema_file',
181 | 'swagger.json')
182 | schema_path = os.path.join(schema_dir, schema_filename)
183 | schema_url = urlparse.urljoin('file:', pathname2url(os.path.abspath(schema_path)))
184 |
185 | handlers = build_http_handlers(None) # don't need http_client for file:
186 | file_handler = handlers['file']
187 | spec_dict = file_handler(schema_url)
188 |
189 | return Spec.from_dict(
190 | spec_dict,
191 | config=create_bravado_core_config(settings),
192 | origin_url=schema_url)
193 |
194 |
195 | def create_bravado_core_config(settings):
196 | """Create a configuration dict for bravado_core based on pyramid_swagger
197 | settings.
198 |
199 | :param settings: pyramid registry settings with configuration for
200 | building a swagger schema
201 | :type settings: dict
202 | :returns: config dict suitable for passing into
203 | bravado_core.spec.Spec.from_dict(..)
204 | :rtype: dict
205 | """
206 | # Map pyramid_swagger config key -> bravado_core config key
207 | config_keys = {
208 | 'pyramid_swagger.enable_request_validation': 'validate_requests',
209 | 'pyramid_swagger.enable_response_validation': 'validate_responses',
210 | 'pyramid_swagger.enable_swagger_spec_validation': 'validate_swagger_spec',
211 | 'pyramid_swagger.use_models': 'use_models',
212 | 'pyramid_swagger.user_formats': 'formats',
213 | 'pyramid_swagger.include_missing_properties': 'include_missing_properties',
214 | }
215 |
216 | configs = {
217 | 'use_models': False,
218 | }
219 | configs.update({
220 | bravado_core_key: settings[pyramid_swagger_key]
221 | for pyramid_swagger_key, bravado_core_key in iteritems(config_keys)
222 | if pyramid_swagger_key in settings
223 | })
224 | configs.update({
225 | key.replace(BRAVADO_CORE_CONFIG_PREFIX, ''): value
226 | for key, value in iteritems(settings)
227 | if key.startswith(BRAVADO_CORE_CONFIG_PREFIX)
228 | })
229 |
230 | return configs
231 |
232 |
233 | def ingest_resources(mapping, schema_dir):
234 | """Consume the Swagger schemas and produce a queryable datastructure.
235 |
236 | :param mapping: Map from resource name to filepath of its api declaration
237 | :type mapping: dict
238 | :param schema_dir: the directory schema files live inside
239 | :type schema_dir: string
240 | :returns: A list of mapping from :class:`RequestMatcher` to
241 | :class:`ValidatorMap`
242 | """
243 | ingested_resources = []
244 | for name, filepath in iteritems(mapping):
245 | try:
246 | ingested_resources.append(load_schema(filepath))
247 | # If we have trouble reading any files, raise a more user-friendly
248 | # error.
249 | except IOError:
250 | raise ApiDeclarationNotFoundError(
251 | 'No api declaration found at {0}. Attempted to load the `{1}` '
252 | 'resource relative to the schema_directory `{2}`. Perhaps '
253 | 'your resource name and API declaration file do not '
254 | 'match?'.format(filepath, name, schema_dir)
255 | )
256 | return ingested_resources
257 |
--------------------------------------------------------------------------------
/tests/acceptance/request_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | import datetime
5 | from contextlib import contextmanager
6 |
7 | import pytest
8 | import simplejson
9 | from pyramid.httpexceptions import exception_response
10 | from webtest.utils import NoDefault
11 |
12 | from pyramid_swagger import exceptions
13 |
14 |
15 | def build_test_app(swagger_versions, **overrides):
16 | """Fixture for setting up a test test_app with particular settings."""
17 | from tests.acceptance.app import main
18 | from webtest import TestApp as App
19 | settings = dict({
20 | 'pyramid_swagger.schema_directory': 'tests/sample_schemas/good_app/',
21 | 'pyramid_swagger.enable_request_validation': True,
22 | 'pyramid_swagger.enable_response_validation': False,
23 | 'pyramid_swagger.enable_swagger_spec_validation': False,
24 | 'pyramid_swagger.swagger_versions': swagger_versions},
25 | **overrides
26 | )
27 |
28 | return App(main({}, **settings))
29 |
30 |
31 | # Parameterize pyramid_swagger.swagger_versions
32 | # Swagger 1.2 tests are broken. Swagger 1.2 is deprecated and thus we have no plans to fix these tests,
33 | # so they have been removed.
34 | @pytest.fixture(
35 | params=[['2.0'], ],
36 | ids=['2.0', ],
37 | )
38 | def test_app(request):
39 | """Fixture for setting up a test test_app with particular settings."""
40 | return build_test_app(
41 | swagger_versions=request.param,
42 | )
43 |
44 |
45 | @contextmanager
46 | def validation_context(request, response=None):
47 | try:
48 | yield
49 | except (
50 | exceptions.RequestValidationError,
51 | exceptions.ResponseValidationError,
52 | exceptions.PathNotFoundError,
53 | ):
54 | raise exception_response(206)
55 | except Exception:
56 | raise exception_response(400)
57 |
58 |
59 | validation_ctx_path = 'tests.acceptance.request_test.validation_context'
60 |
61 |
62 | def test_echo_date_with_pyramid_swagger_renderer(test_app):
63 | input_object = {'date': datetime.date.today().isoformat()}
64 |
65 | response = test_app.post_json('/echo_date', input_object)
66 |
67 | # If the request is served via Swagger1.2
68 | assert response.status_code == 200
69 | assert response.json == input_object
70 |
71 |
72 | def test_echo_date_with_json_renderer(test_app):
73 | today = datetime.date.today()
74 | input_object = {'date': today.isoformat()}
75 |
76 | exc = None
77 | response = None
78 | try:
79 | response = test_app.post_json('/echo_date_json_renderer', input_object)
80 | except TypeError as exception:
81 | exc = exception
82 |
83 | served_swagger_versions = test_app.app.registry.settings['pyramid_swagger.swagger_versions']
84 |
85 | if '2.0' in served_swagger_versions:
86 | # If the request is served via Swagger2.0, pyramid_swagger will perform types
87 | # conversions providing a datetime.date object in the pyramid view
88 | assert exc.args == ('{!r} is not JSON serializable'.format(today), )
89 | else:
90 | # If the request is served via Swagger1.2 there are no implicit type conversions performed by pyramid_swagger
91 | assert response.status_code == 200
92 | assert response.json == input_object
93 |
94 |
95 | @pytest.mark.parametrize(
96 | 'body, expected_length',
97 | [
98 | [NoDefault, 0],
99 | [{}, 2],
100 | ],
101 | )
102 | def test_post_endpoint_with_optional_body(test_app, body, expected_length):
103 | assert test_app.post_json('/post_endpoint_with_optional_body', body).json == expected_length
104 |
105 |
106 | def test_200_with_form_params(test_app):
107 | assert test_app.post(
108 | '/post_with_form_params',
109 | {'form_param': 42},
110 | ).status_code == 200
111 |
112 |
113 | def test_200_with_file_upload(test_app):
114 | assert test_app.post(
115 | '/post_with_file_upload',
116 | upload_files=[('photo_file', 'photo.jpg', b'')],
117 | ).status_code == 200
118 |
119 |
120 | def test_400_with_form_params_wrong_type(test_app):
121 | assert test_app.post(
122 | '/post_with_form_params',
123 | {'form_param': "not a number"},
124 | expect_errors=True,
125 | ).status_code == 400
126 |
127 |
128 | def test_400_if_json_body_for_form_parms(test_app):
129 | assert test_app.post_json(
130 | '/post_with_form_params',
131 | {'form_param': 42},
132 | expect_errors=True,
133 | ).status_code == 400
134 |
135 |
136 | def test_400_if_required_query_args_absent(test_app):
137 | assert test_app.get(
138 | '/sample/path_arg1/resource',
139 | expect_errors=True,
140 | ).status_code == 400
141 |
142 |
143 | def test_200_if_optional_query_args_absent(test_app):
144 | assert test_app.get(
145 | '/sample/path_arg1/resource',
146 | params={'required_arg': 'test'}, # no `optional_arg` arg
147 | ).status_code == 200
148 |
149 |
150 | def test_200_if_request_arg_is_wrong_type(test_app):
151 | assert test_app.get(
152 | '/sample/path_arg1/resource',
153 | params={'required_arg': 1.0},
154 | ).status_code == 200
155 |
156 |
157 | def test_200_if_request_arg_types_are_not_strings(test_app):
158 | assert test_app.get(
159 | '/get_with_non_string_query_args',
160 | params={
161 | 'int_arg': '5',
162 | 'float_arg': '3.14',
163 | 'boolean_arg': 'true',
164 | },
165 | ).status_code == 200
166 |
167 |
168 | def test_404_if_path_not_in_swagger(test_app):
169 | assert test_app.get(
170 | '/undefined/path',
171 | expect_errors=True,
172 | ).status_code == 404
173 |
174 |
175 | def test_200_skip_validation_with_excluded_path():
176 | app = build_test_app(
177 | swagger_versions=['2.0'],
178 | **{'pyramid_swagger.exclude_paths': [r'^/undefined/path']}
179 | )
180 | assert app.get('/undefined/path').status_code == 200
181 |
182 |
183 | def test_400_if_request_arg_is_wrong_type_but_not_castable(test_app):
184 | assert test_app.get(
185 | '/get_with_non_string_query_args',
186 | params={'float_arg': 'foobar'},
187 | expect_errors=True,
188 | ).status_code == 400
189 |
190 |
191 | def test_400_if_path_arg_not_valid_enum(test_app):
192 | assert test_app.get(
193 | '/sample/invalid_arg/resource',
194 | params={'required_arg': 'test'},
195 | expect_errors=True,
196 | ).status_code == 400
197 |
198 |
199 | def test_200_if_path_arg_is_wrong_type_but_castable(test_app):
200 | assert test_app.get(
201 | '/sample/nonstring/3/1.4/false',
202 | ).status_code == 200
203 |
204 |
205 | def test_400_if_required_body_is_missing(test_app):
206 | assert test_app.post_json(
207 | '/sample',
208 | {},
209 | expect_errors=True,
210 | ).status_code == 400
211 |
212 |
213 | def test_200_on_json_body_without_contenttype_header(test_app):
214 | """See https://github.com/striglia/pyramid_swagger/issues/49."""
215 | # We use .post to avoid sending a Content Type of application/json.
216 | assert test_app.post(
217 | '/sample?optional_string=bar',
218 | simplejson.dumps({'foo': 'test'}),
219 | {'Content-Type': ''},
220 | ).status_code == 200
221 |
222 |
223 | def test_400_if_body_has_missing_required_arg(test_app):
224 | assert test_app.post_json(
225 | '/sample',
226 | {'bar': 'test'},
227 | expect_errors=True,
228 | ).status_code == 400
229 |
230 |
231 | def test_200_if_body_has_missing_optional_arg(test_app):
232 | assert test_app.post_json(
233 | '/sample',
234 | {'foo': 'test'},
235 | ).status_code == 200
236 |
237 |
238 | def test_200_if_required_body_is_model(test_app):
239 | assert test_app.post_json(
240 | '/sample',
241 | {'foo': 'test', 'bar': 'test'},
242 | ).status_code == 200
243 |
244 |
245 | def test_200_if_required_body_is_primitives(test_app):
246 | assert test_app.post_json(
247 | '/post_with_primitive_body',
248 | ['foo', 'bar'],
249 | ).status_code == 200
250 |
251 |
252 | def test_400_if_extra_body_args(test_app):
253 | assert test_app.post_json(
254 | '/sample',
255 | {'foo': 'test', 'bar': 'test', 'made_up_argument': 1},
256 | expect_errors=True,
257 | ).status_code == 400
258 |
259 |
260 | def test_400_if_extra_query_args(test_app):
261 | assert test_app.get(
262 | '/sample/path_arg1/resource?made_up_argument=1',
263 | expect_errors=True,
264 | ).status_code == 400
265 |
266 |
267 | def test_400_if_missing_required_header(test_app):
268 | assert test_app.get(
269 | '/sample/header',
270 | expect_errors=True,
271 | ).status_code == 400
272 |
273 |
274 | def test_200_with_required_header(test_app):
275 | response = test_app.get(
276 | '/sample/header',
277 | headers={'X-Force': 'True'},
278 | expect_errors=True,
279 | )
280 | assert response.status_code == 200
281 |
282 |
283 | def test_200_skip_validation_when_disabled():
284 | # calling endpoint with required args missing
285 | overrides = {
286 | 'pyramid_swagger.enable_request_validation': False,
287 | 'skip_swagger_data_assert': True,
288 | }
289 | app = build_test_app(
290 | swagger_versions=['2.0'],
291 | **overrides
292 | )
293 | response = app.get('/get_with_non_string_query_args', params={})
294 | assert response.status_code == 200
295 |
296 |
297 | def test_path_validation_context():
298 | app = build_test_app(
299 | swagger_versions=['2.0'],
300 | **{'pyramid_swagger.validation_context_path': validation_ctx_path}
301 | )
302 | assert app.get('/does_not_exist').status_code == 206
303 |
304 |
305 | def test_request_validation_context():
306 | app = build_test_app(
307 | swagger_versions=['2.0'],
308 | **{'pyramid_swagger.validation_context_path': validation_ctx_path})
309 | response = app.get('/get_with_non_string_query_args', params={})
310 | assert response.status_code == 206
311 |
312 |
313 | def test_request_to_authenticated_endpoint_without_authentication():
314 | app = build_test_app(swagger_versions=['2.0'])
315 | response = app.get(
316 | '/sample/authentication',
317 | expect_errors=True,
318 | )
319 | assert response.status_code == 401
320 |
321 |
322 | def test_request_to_endpoint_with_no_response_schema():
323 | app = build_test_app(swagger_versions=['2.0'])
324 | response = app.get('/sample/no_response_schema')
325 | assert response.status_code == 200
326 |
--------------------------------------------------------------------------------
/pyramid_swagger/load_schema.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Module to load swagger specs and build efficient data structures for querying
4 | them during request validation.
5 | """
6 | from __future__ import absolute_import
7 | from __future__ import unicode_literals
8 |
9 | from collections import namedtuple
10 | from copy import deepcopy
11 |
12 | import jsonschema
13 | import simplejson
14 | from jsonschema import RefResolver
15 | from jsonschema import validators
16 | from jsonschema.exceptions import ValidationError
17 | from jsonschema.validators import Draft3Validator
18 | from jsonschema.validators import Draft4Validator
19 | from six import iteritems
20 |
21 | from pyramid_swagger.model import partial_path_match
22 |
23 |
24 | EXTENDED_TYPES = {
25 | 'number': (float,),
26 | 'float': (float,),
27 | 'int': (int,),
28 | }
29 |
30 |
31 | _draft3_type_validator = Draft3Validator.VALIDATORS['type']
32 | _draft4_type_validator = Draft4Validator.VALIDATORS['type']
33 | _draft4_required_validator = Draft4Validator.VALIDATORS['required']
34 | _ref_validator = Draft4Validator.VALIDATORS['$ref']
35 |
36 |
37 | def build_param_schema(schema, param_type):
38 | """Turn a swagger endpoint schema into an equivalent one to validate our
39 | request.
40 |
41 | As an example, this would take this swagger schema:
42 | {
43 | "paramType": "query",
44 | "name": "query",
45 | "description": "Location to query",
46 | "type": "string",
47 | "required": true
48 | }
49 | To this jsonschema:
50 | {
51 | "type": "object",
52 | "additionalProperties": "False",
53 | "properties:": {
54 | "description": "Location to query",
55 | "type": "string",
56 | "required": true
57 | }
58 | }
59 | Which we can then validate against a JSON object we construct from the
60 | pyramid request.
61 | """
62 | properties = filter_params_by_type(schema, param_type)
63 | if not properties:
64 | return
65 |
66 | # Generate a jsonschema that describes the set of all query parameters. We
67 | # can then validate this against dict(request.params).
68 | return {
69 | 'type': 'object',
70 | 'properties': dict((p['name'], p) for p in properties),
71 | # Allow extra headers. Most HTTP requests will have headers which
72 | # are outside the scope of the spec (like `Host`, or `User-Agent`)
73 | 'additionalProperties': param_type == 'header',
74 | }
75 |
76 |
77 | def filter_params_by_type(schema, param_type):
78 | return [s for s in schema['parameters'] if s['paramType'] == param_type]
79 |
80 |
81 | def extract_body_schema(schema):
82 | """Return the body parameter schema from an operation schema."""
83 | matching_body_schemas = filter_params_by_type(schema, 'body')
84 | # There can be only one body param
85 | return matching_body_schemas[0] if matching_body_schemas else None
86 |
87 |
88 | def ignore(_validator, *args):
89 | """A validator which performs no validation. Used to `ignore` some schema
90 | fields during validation.
91 | """
92 | return
93 |
94 |
95 | def build_swagger_type_validator(models):
96 | def swagger_type_validator(validator, ref, instance, schema):
97 | if ref in models:
98 | return _ref_validator(validator, ref, instance, schema)
99 | else:
100 | return _draft4_type_validator(validator, ref, instance, schema)
101 |
102 | return swagger_type_validator
103 |
104 |
105 | def type_validator(validator, types, instance, schema):
106 | """Swagger 1.2 supports parameters of 'type': 'File'. Skip validation of
107 | the 'type' field in this case.
108 | """
109 | if schema.get('type') == 'File':
110 | return []
111 | return _draft3_type_validator(validator, types, instance, schema)
112 |
113 |
114 | def required_validator(validator, req, instance, schema):
115 | """Swagger 1.2 expects `required` to be a bool in the Parameter object, but
116 | a list of properties in a Model object.
117 | """
118 | if schema.get('paramType'):
119 | if req is True and not instance:
120 | return [ValidationError("%s is required" % schema['name'])]
121 | return []
122 | return _draft4_required_validator(validator, req, instance, schema)
123 |
124 |
125 | def get_body_validator(models):
126 | """Returns a validator for the request body, based on a
127 | :class:`jsonschema.validators.Draft4Validator`, with extra validations
128 | added for swaggers extensions to jsonschema.
129 |
130 | :param models: a mapping of reference to models
131 | :returns: a :class:`jsonschema.validators.Validator` which can validate
132 | the request body.
133 | """
134 | return validators.extend(
135 | Draft4Validator,
136 | {
137 | 'paramType': ignore,
138 | 'name': ignore,
139 | 'type': build_swagger_type_validator(models),
140 | 'required': required_validator,
141 | }
142 | )
143 |
144 |
145 | Swagger12ParamValidator = validators.extend(
146 | Draft3Validator,
147 | {
148 | 'paramType': ignore,
149 | 'name': ignore,
150 | 'type': type_validator,
151 | }
152 | )
153 |
154 |
155 | class ValidatorMap(
156 | namedtuple('_VMap', 'query path form headers body response')
157 | ):
158 | """
159 | A data object with validators for each part of the request and response
160 | objects. Each field is a :class:`SchemaValidator`.
161 | """
162 | __slots__ = ()
163 |
164 | @classmethod
165 | def from_operation(cls, operation, models, resolver):
166 | args = []
167 | for schema, validator in [
168 | (build_param_schema(operation, 'query'), Swagger12ParamValidator),
169 | (build_param_schema(operation, 'path'), Swagger12ParamValidator),
170 | (build_param_schema(operation, 'form'), Swagger12ParamValidator),
171 | (build_param_schema(operation, 'header'), Swagger12ParamValidator),
172 | (extract_body_schema(operation), get_body_validator(models)),
173 | (extract_response_body_schema(operation, models),
174 | Draft4Validator),
175 | ]:
176 | args.append(SchemaValidator.from_schema(
177 | schema,
178 | resolver,
179 | validator))
180 |
181 | return cls(*args)
182 |
183 |
184 | class SchemaValidator(object):
185 | """A Validator used by :mod:`pyramid_swagger.tween` to validate a
186 | field from the request or response.
187 |
188 | :param schema: a :class:`dict` jsonschema that was used by the
189 | validator
190 | :param validator: a Validator which a func:`validate` method
191 | for validating a field from a request or response. This
192 | will often be a :class:`jsonschema.validator.Validator`.
193 | """
194 |
195 | def __init__(self, schema, validator):
196 | self.schema = schema
197 | self.validator = validator
198 |
199 | @classmethod
200 | def from_schema(cls, schema, resolver, validator_class):
201 | type_checker = deepcopy(validator_class.TYPE_CHECKER)
202 | type_checker.redefine_many({
203 | type_name: lambda checker, value: all(check(value) for check in checks)
204 | for type_name, checks in iteritems(EXTENDED_TYPES)
205 | })
206 | extended_validator_class = jsonschema.validators.extend(
207 | validator_class,
208 | type_checker=type_checker,
209 | )
210 | return cls(
211 | schema,
212 | extended_validator_class(schema, resolver=resolver))
213 |
214 | def validate(self, values):
215 | """Validate a :class:`dict` of values. If `self.schema` is falsy this
216 | is a noop.
217 | """
218 | if not self.schema or (values is None and not self.schema.get('required', False)):
219 | return
220 | self.validator.validate(values)
221 |
222 |
223 | def build_request_to_validator_map(schema, resolver):
224 | """Build a mapping from :class:`RequestMatcher` to :class:`ValidatorMap`
225 | for each operation in the API spec. This mapping may be used to retrieve
226 | the appropriate validators for a request.
227 | """
228 | schema_models = schema.get('models', {})
229 | return dict(
230 | (
231 | RequestMatcher(api['path'], operation['method']),
232 | ValidatorMap.from_operation(operation, schema_models, resolver)
233 | )
234 | for api in schema['apis']
235 | for operation in api['operations']
236 | )
237 |
238 |
239 | class RequestMatcher(object):
240 | """Match a :class:`pyramid.request.Request` to a swagger Operation"""
241 |
242 | def __init__(self, path, method):
243 | self.path = path
244 | self.method = method
245 |
246 | def matches(self, request):
247 | """
248 | :param request: a :class:`pyramid.request.Request`
249 | :returns: True if this matcher matches the request, False otherwise
250 | """
251 | return partial_path_match(request.path_info, self.path) and request.method == self.method
252 |
253 |
254 | def extract_response_body_schema(operation, schema_models):
255 | if operation['type'] in schema_models:
256 | return extract_validatable_type(operation['type'], schema_models)
257 |
258 | acceptable_fields = (
259 | 'type', '$ref', 'format', 'defaultValue', 'enum', 'minimum',
260 | 'maximum', 'items', 'uniqueItems'
261 | )
262 |
263 | return dict(
264 | (field, operation[field])
265 | for field in acceptable_fields
266 | if field in operation
267 | )
268 |
269 |
270 | def extract_validatable_type(type_name, models):
271 | """Returns a jsonschema-compatible typename from the Swagger type.
272 |
273 | This is necessary because for our Swagger specs to be compatible with
274 | swagger-ui, they must not use a $ref to internal models.
275 |
276 | :returns: A key-value that jsonschema can validate. Key will be either
277 | 'type' or '$ref' as is appropriate.
278 | :rtype: dict
279 | """
280 | if type_name in models:
281 | return {'$ref': type_name}
282 | else:
283 | return {'type': type_name}
284 |
285 |
286 | def load_schema(schema_path):
287 | """Prepare the api specification for request and response validation.
288 |
289 | :returns: a mapping from :class:`RequestMatcher` to :class:`ValidatorMap`
290 | for every operation in the api specification.
291 | :rtype: dict
292 | """
293 | with open(schema_path, 'r') as schema_file:
294 | schema = simplejson.load(schema_file)
295 | resolver = RefResolver('', '', schema.get('models', {}))
296 | return build_request_to_validator_map(schema, resolver)
297 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 | 2.9.0 (2024-05-08)
4 | ++++++++++++++++++++++++++
5 | * Add response_validation_exclude_routes support (see #253)
6 |
7 | 2.8.0 (2024-05-03)
8 | ++++++++++++++++++++++++++
9 | * Ensure http 401 in case of missing security (see #239)
10 | * Fix pyramid swagger renderer if missing schema (see #242)
11 | * Reduce usage of deprecated methods (see #241)
12 | * Minor fixes (see #243, #244 and #246)
13 | * Update tox to py38 and py310 and skip swagger 1.2 tests (see #249)
14 |
15 | 2.7.0 (2019-05-07)
16 | ++++++++++++++++++++++++++
17 | * Remove not needed deprecation warnings (see #238)
18 | * Make ``pyramid_swagger`` compatible with ``jsonschema>3`` (see #327)
19 |
20 | 2.6.2 (2018-07-02)
21 | ++++++++++++++++++++++++++
22 | * Fix bug that prevents library usage on Windows Platform (see #234)
23 |
24 | 2.6.1 (2018-05-24)
25 | ++++++++++++++++++++++++++
26 | * Fix operation extraction in `PyramidSwaggerRendererFactory` in case of Swagger1.2 endpoint (see #230)
27 | * Fix request body extraction if body is not set. (see #231)
28 |
29 | 2.6.0 (2017-11-14)
30 | ++++++++++++++++++++++++++
31 | * Support setting bravado-core config values by prefixing them with ``bravado_core.`` in the pyramid_swagger config (see #221)
32 | * Support raw_bytes response attribute, required for msgpack wire format support in outgoing responses (see #222)
33 |
34 | 2.5.0 (2017-10-26)
35 | ++++++++++++++++++++++++++
36 | * Support `include_missing_properties` bravado-core flag in pyramid configuration
37 | * Outsource flattening logic to bravado-core library.
38 | * Expose bravado-core ``operation`` in request object
39 | * Add ``pyramid_renderer`` and ``PyramidSwaggerRendererFactory``
40 |
41 | 2.4.1 (2017-06-14)
42 | ++++++++++++++++++++++++++
43 | * Bugfix: add a quick fix to prevent resolve_refs from making empty json keys on external refs (see #206)
44 |
45 | 2.4.0 (2017-05-30)
46 | ++++++++++++++++++++++++++
47 | * Bugfix: prevent resolve_refs from resolution failures when flattening specs with recursive $refs (see #204)
48 | * Allow serving of api_docs from paths besides /api_docs (see #187)
49 | * Support virtual hosting via SCRIPT_NAME (see #201 and https://www.python.org/dev/peps/pep-0333/)
50 |
51 | 2.3.2 (2017-04-10)
52 | ++++++++++++++++++
53 | * Fix reading configuration values from INI files (see #182, #200)
54 |
55 | 2.3.1 (2017-03-27)
56 | ++++++++++++++++++
57 | * Fix validation context for swagger 2.0 requests
58 | * Added docs for validation context
59 | * Preserved original exception when reraising for validation context exceptions
60 | * Remove support for Python 2.6, newer Pyramid versions don't support it either
61 | * Fix issue with missing content type when using webob >= 1.7 (see #185)
62 |
63 | 2.3.0 (2016-09-27)
64 | ++++++++++++++++++
65 | * Fix installation with Python 3 on systems using a POSIX/ASCII locale.
66 |
67 | 2.3.0-rc3 (2016-06-28)
68 | ++++++++++++++++++++++
69 | * Adds ``dereference_served_schema`` config flag to force served spec to be a
70 | single file. Useful for avoiding mixed-spec inconsistencies when running
71 | multiple versions of your service simultaneously.
72 |
73 | 2.3.0-rc2 (2016-05-09)
74 | ++++++++++++++++++++++
75 | * Add ability for a single spec to serve YAML or JSON to clients
76 | * Support multi-file local specs, serving them over multiple HTTP endpoints
77 | * Improve Swagger validation messages when Pyramid cannot find your route (see #163)
78 | * Bugfix: responses with headers in the spec no longer break request validation (see #159)
79 |
80 | 2.3.0-rc1 (2016-03-21)
81 | ++++++++++++++++++++++
82 | * Support for YAML spec files
83 | * Bugfix: remove extraneous x-scope in digested spec (see https://github.com/Yelp/bravado-core/issues/78)
84 |
85 | 2.2.3 (2016-02-09)
86 | ++++++++++++++++++++++
87 | * Restore testing of py3x versions
88 | * Support pyramid 1.6 and beyond.
89 | * Support specification of routes using route_prefix
90 |
91 | 2.2.2 (2015-10-12)
92 | ++++++++++++++++++++++
93 | * Upgrade to bravado-core 3.0.0, which includes a change in the way user-defined formats are registered. See the `Bravado 3.0.0 changelog entry`_ for more detail.
94 |
95 | .. _Bravado 3.0.0 changelog entry: http://github.com/Yelp/bravado-core/blob/master/CHANGELOG.rst
96 |
97 |
98 | 2.2.1 (2015-08-20)
99 | ++++++++++++++++++++++
100 | * No longer attempts to validate error responses, which typically don't follow
101 | the same format as successful responses. (Closes: #121)
102 |
103 | 2.2.0 (2015-08-19)
104 | ++++++++++++++++++++++
105 | * Added ``prefer_20_routes`` configuration option to ease incremental migrations from v1.2 to
106 | v2.0. (See :ref:`prefer20migration`)
107 |
108 | 2.1.0 (2015-08-14)
109 | ++++++++++++++++++++++
110 | * Added ``user_formats`` configuration option to provide user-defined formats which can be used for validations
111 | and conversions to wire-python-wire formats. (See :ref:`user-format-label`)
112 | * Added support for relative cross-refs in Swagger v2.0 specs.
113 |
114 | 2.0.0 (2015-06-25)
115 | ++++++++++++++++++++++
116 | * Added ``use_models`` configuration option for Swagger 2.0 backwards compatibility with existing pyramid views
117 |
118 | 2.0.0-rc2 (2015-05-26)
119 | ++++++++++++++++++++++
120 | * Upgraded bravado-core to 1.0.0-rc1 so basePath is used when matching a request to an operation
121 | * Updates for refactored SwaggerError exception hierarchy in bravado-core
122 | * Fixed file uploads that use Content-Type: multipart/form-data
123 |
124 | 2.0.0-rc1 (2015-05-13)
125 | ++++++++++++++++++++++
126 |
127 | **Backwards Incompatible**
128 |
129 | * Support for Swagger 2.0 - See `Migrating to Swagger 2.0`_
130 |
131 | .. _Migrating to Swagger 2.0: http://pyramid-swagger.readthedocs.org/en/latest/migrating_to_swagger_20.html
132 |
133 | 1.5.0 (2015-05-12)
134 | ++++++++++++++++++++++
135 |
136 | * Now using swagger_spec_validator package for spec validation. Should be far
137 | more robust than the previous implementation.
138 |
139 | 1.5.0-rc2 (2015-04-1)
140 | ++++++++++++++++++++++
141 |
142 | * Form-encoded bodies are now validated correctly.
143 | * Fixed bug in `required` swagger attribute handling.
144 |
145 | 1.5.0-rc1 (2015-03-30)
146 | ++++++++++++++++++++++
147 |
148 | * Added ``enable_api_docs_views`` configuration option so /api-docs
149 | auto-registration can be disabled in situations where users want to serve
150 | the Swagger spec in a nonstandard way.
151 | * Added ``exclude_routes`` configuration option. Allows a blacklist of Pyramid
152 | routes which will be ignored for validation purposes.
153 | * Added ``generate_resource_listing`` configuration option to allow
154 | pyramid_swagger to generate the ``apis`` section of the resource listing.
155 | * Bug fix for issues relating to ``void`` responses (See `Issue 79`_)
156 | * Added support for header validation.
157 | * Make casted values from the request available through
158 | ``request.swagger_data``
159 |
160 | .. _Issue 79: https://github.com/striglia/pyramid_swagger/issues/79
161 |
162 | 1.4.0 (2015-01-27)
163 | ++++++++++++++++++
164 |
165 | * Added ``validation_context_path`` setting which allows the user to specify a
166 | path to a contextmanager to custom handle request/response validation
167 | exceptions.
168 |
169 | 1.3.0 (2014-12-02)
170 | ++++++++++++++++++
171 |
172 | * Now throws RequestValidationError and ResponseValidationError instead of
173 | HTTPClientError and HTTPInternalServerError respectively. The new errors
174 | subclass the old ones for backwards compatibility.
175 |
176 | 1.2.0 (2014-10-21)
177 | ++++++++++++++++++
178 |
179 | * Added ``enable_request_validation`` setting which toggles whether request
180 | content is validated.
181 | * Added ``enable_path_validation`` setting which toggles whether HTTP calls to
182 | endpoints will 400 if the URL is not described in the Swagger schema. If this
183 | flag is disabled and the path is not found, no validation of any kind is
184 | performed by pyramid-swagger.
185 | * Added ``exclude_paths`` setting which duplicates the functionality of
186 | `skip_validation`. `skip_validation` is deprecated and scheduled for removal
187 | in the 2.0.0 release.
188 | * Adds LICENSE file
189 | * Fixes misuse of webtest which could cause ``make test`` to pass while
190 | functionality was broken.
191 |
192 | 1.1.1 (2014-08-26)
193 | ++++++++++++++++++
194 |
195 | * Fixes bug where response bodies were not validated correctly unless they were
196 | a model or primitive type.
197 | * Fixes bug where POST bodies could be mis-parsed as query arguments.
198 | * Better backwards compatibility warnings in this changelog!
199 |
200 | 1.1.0 (2014-07-14)
201 | ++++++++++++++++++
202 |
203 | * Swagger schema directory defaults to ``api_docs/`` rather than being a required
204 | configuration line.
205 | * If the resource listing or API declarations are not at the filepaths
206 | expected, readable errors are raised.
207 | * This changelog is now a part of the build documentation and backfilled to the
208 | initial package version.
209 |
210 |
211 | 1.0.0 (2014-07-08)
212 | ++++++++++++++++++
213 |
214 | **Backwards Incompatible**
215 |
216 | * Initial fully functional release.
217 | * Your service now must supply both a resource listing and all accompanying api
218 | declarations.
219 | * Swagger schemas are automatically served out of ``/api-docs`` by including the
220 | library.
221 | * The api declaration basepath returned by hitting ``/api-docs/foo`` is guaranteed
222 | to be ``Pyramid.request.application_url``.
223 | * Void return types are now checked.
224 |
225 |
226 | 0.5.0 (2014-07-08)
227 | ++++++++++++++++++
228 |
229 | * Added configurable list of regular expressions to not validate
230 | requests/responses against.
231 | * Vastly improved documentation! Includes a quickstart for those new to the
232 | library.
233 | * Adds coverage and code health badges to README
234 |
235 |
236 | 0.4.0 (2014-06-20)
237 | ++++++++++++++++++
238 |
239 | * Request validation now works with path arguments.
240 | * True acceptance testing implemented for all known features. Much improved
241 | coverage.
242 |
243 | 0.4.0 (2014-06-20)
244 | ++++++++++++++++++
245 |
246 | * True acceptance testing implemented for all known features. Much improved
247 | coverage.
248 |
249 | 0.3.2 (2014-06-16)
250 | ++++++++++++++++++
251 |
252 | * HEAD is now an allowed HTTP method
253 |
254 | 0.3.1 (2014-06-16)
255 | ++++++++++++++++++
256 |
257 | * Swagger spec is now validated on startup
258 | * Fixes bug where multiple methods with the same URL were not resolved properly
259 | * Fixes bug with validating non-string args in paths and query args
260 | * Fixes bug with referencing models from POST bodies
261 |
262 | 0.3.0 (2014-05-29)
263 | ++++++++++++++++++
264 |
265 | * Response validation can be disabled via configuration
266 | * Supports Python 3.3 and 3.4!
267 |
268 | 0.2.2 (2014-05-28)
269 | ++++++++++++++++++
270 |
271 | * Adds readthedocs links, travis badge to README
272 | * Requests missing bodies return 400 instead of causing tracebacks
273 |
274 | 0.2.1 (2014-05-15)
275 | ++++++++++++++++++
276 |
277 | * Requests to non-existant endpoints now return 400 errors
278 |
279 | 0.1.1 (2014-05-13)
280 | ++++++++++++++++++
281 |
282 | * Build docs now live at ``docs/build/html``
283 |
284 | 0.1.0 (2014-05-12)
285 | ++++++++++++++++++
286 |
287 | * Initial version. Supports very basic validation of incoming requests.
288 |
--------------------------------------------------------------------------------
/tests/acceptance/response_test.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import absolute_import
3 |
4 | from contextlib import contextmanager
5 |
6 | import mock
7 | import pyramid.testing
8 | import pytest
9 | import simplejson
10 | from pyramid.config import Configurator
11 | from pyramid.interfaces import IRoutesMapper
12 | from pyramid.registry import Registry
13 | from pyramid.response import Response
14 | from pyramid.urldispatch import RoutesMapper
15 | from webob.multidict import MultiDict
16 | from webtest import AppError
17 |
18 | import pyramid_swagger.tween
19 | from pyramid_swagger.exceptions import ResponseValidationError
20 | from pyramid_swagger.ingest import compile_swagger_schema
21 | from pyramid_swagger.ingest import get_resource_listing
22 | from pyramid_swagger.tween import validation_tween_factory
23 | from tests.acceptance.request_test import build_test_app
24 |
25 |
26 | class CustomResponseValidationException(Exception):
27 | pass
28 |
29 |
30 | class EnhancedDummyRequest(pyramid.testing.DummyRequest):
31 | """
32 | pyramid.testing.DummyRequest doesn't support MultiDicts like the real
33 | pyramid.request.Request so this is the next best thing.
34 | """
35 |
36 | def __init__(self, **kw):
37 | super(EnhancedDummyRequest, self).__init__(**kw)
38 | self.GET = MultiDict(self.GET)
39 | # Make sure content_type attr exists is not passed in via **kw
40 | self.content_type = getattr(self, 'content_type', None)
41 |
42 |
43 | @contextmanager
44 | def validation_context(request, response=None):
45 | try:
46 | yield
47 | except Exception:
48 | raise CustomResponseValidationException
49 |
50 |
51 | validation_ctx_path = 'tests.acceptance.response_test.validation_context'
52 |
53 |
54 | def get_registry(settings):
55 | registry = Registry('testing')
56 | config = Configurator(registry=registry)
57 | if getattr(registry, 'settings', None) is None:
58 | config._set_settings(settings)
59 | registry.registerUtility(RoutesMapper(), IRoutesMapper)
60 | config.commit()
61 | return registry
62 |
63 |
64 | def get_swagger_schema(schema_dir='tests/sample_schemas/good_app/'):
65 | return compile_swagger_schema(
66 | schema_dir,
67 | get_resource_listing(schema_dir, False)
68 | )
69 |
70 |
71 | def _validate_against_tween(request, response=None, **overrides):
72 | """
73 | Acceptance testing helper for testing the validation tween with Swagger 1.2
74 | responses.
75 |
76 | :param request: pytest fixture
77 | :param response: standard fixture by default
78 | """
79 | def handler(request):
80 | return response or Response()
81 |
82 | settings = dict({
83 | 'pyramid_swagger.swagger_versions': ['1.2'],
84 | 'pyramid_swagger.enable_swagger_spec_validation': False,
85 | 'pyramid_swagger.schema_directory': 'tests/sample_schemas/good_app/'},
86 | **overrides
87 | )
88 | settings['pyramid_swagger.schema12'] = get_swagger_schema()
89 | settings['pyramid_swagger.schema20'] = None
90 | registry = get_registry(settings)
91 |
92 | # Let's make request validation a no-op so we can focus our tests.
93 | with mock.patch.object(pyramid_swagger.tween, 'validate_request'):
94 | validation_tween_factory(handler, registry)(request)
95 |
96 |
97 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
98 | def test_response_validation_enabled_by_default():
99 | request = EnhancedDummyRequest(
100 | method='GET',
101 | path='/sample/path_arg1/resource',
102 | params={'required_arg': 'test'},
103 | matchdict={'path_arg': 'path_arg1'},
104 | )
105 | # Omit the logging_info key from the response. If response validation
106 | # occurs, we'll fail it.
107 | response = Response(
108 | body=simplejson.dumps({'raw_response': 'foo'}),
109 | headers={'Content-Type': 'application/json; charset=UTF-8'},
110 | )
111 | with pytest.raises(ResponseValidationError) as excinfo:
112 | _validate_against_tween(request, response=response)
113 | assert "'logging_info' is a required property" in str(excinfo.value)
114 |
115 |
116 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
117 | def test_500_when_response_is_missing_required_field():
118 | request = EnhancedDummyRequest(
119 | method='GET',
120 | path='/sample/path_arg1/resource',
121 | params={'required_arg': 'test'},
122 | matchdict={'path_arg': 'path_arg1'},
123 | )
124 | # Omit the logging_info key from the response.
125 | response = Response(
126 | body=simplejson.dumps({'raw_response': 'foo'}),
127 | headers={'Content-Type': 'application/json; charset=UTF-8'},
128 | )
129 | with pytest.raises(ResponseValidationError) as excinfo:
130 | _validate_against_tween(request, response=response)
131 | assert "'logging_info' is a required property" in str(excinfo.value)
132 |
133 |
134 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
135 | def test_200_when_response_is_void_with_none_response():
136 | request = EnhancedDummyRequest(
137 | method='GET',
138 | path='/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}',
139 | params={'required_arg': 'test'},
140 | matchdict={'int_arg': '1', 'float_arg': '2.0', 'boolean_arg': 'true'},
141 | )
142 | response = Response(
143 | body=simplejson.dumps(None),
144 | headers={'Content-Type': 'application/json; charset=UTF-8'},
145 | )
146 | _validate_against_tween(request, response=response)
147 |
148 |
149 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
150 | def test_200_when_response_is_void_with_empty_response():
151 | request = EnhancedDummyRequest(
152 | method='GET',
153 | path='/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}',
154 | params={'required_arg': 'test'},
155 | matchdict={'int_arg': '1', 'float_arg': '2.0', 'boolean_arg': 'true'},
156 | )
157 | response = Response(body='{}')
158 | _validate_against_tween(request, response=response)
159 |
160 |
161 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
162 | def test_500_when_response_arg_is_wrong_type():
163 | request = EnhancedDummyRequest(
164 | method='GET',
165 | path='/sample/path_arg1/resource',
166 | params={'required_arg': 'test'},
167 | matchdict={'path_arg': 'path_arg1'},
168 | )
169 | response = Response(
170 | body=simplejson.dumps({
171 | 'raw_response': 1.0,
172 | 'logging_info': {'foo': 'bar'}
173 | }),
174 | headers={'Content-Type': 'application/json; charset=UTF-8'},
175 | )
176 | with pytest.raises(ResponseValidationError) as excinfo:
177 | _validate_against_tween(request, response=response)
178 | assert "1.0 is not of type " in str(excinfo.value)
179 |
180 |
181 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
182 | def test_500_for_bad_validated_array_response():
183 | request = EnhancedDummyRequest(
184 | method='GET',
185 | path='/sample_array_response',
186 | )
187 | response = Response(
188 | body=simplejson.dumps([{"enum_value": "bad_enum_value"}]),
189 | headers={'Content-Type': 'application/json; charset=UTF-8'},
190 | )
191 | with pytest.raises(ResponseValidationError) as excinfo:
192 | _validate_against_tween(request, response=response)
193 | assert "is not one of [" in str(excinfo.value)
194 |
195 |
196 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
197 | def test_200_for_good_validated_array_response():
198 | request = EnhancedDummyRequest(
199 | method='GET',
200 | path='/sample_array_response',
201 | )
202 | response = Response(
203 | body=simplejson.dumps([{"enum_value": "good_enum_value"}]),
204 | headers={'Content-Type': 'application/json; charset=UTF-8'},
205 | )
206 |
207 | _validate_against_tween(request, response=response)
208 |
209 |
210 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
211 | def test_200_for_normal_response_validation():
212 | app = build_test_app(
213 | swagger_versions=['1.2'],
214 | **{'pyramid_swagger.enable_response_validation': True}
215 | )
216 | response = app.post_json('/sample', {'foo': 'test', 'bar': 'test'})
217 | assert response.status_code == 200
218 |
219 |
220 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
221 | def test_200_skip_validation_for_excluded_path():
222 | # FIXME(#64): This test is broken and doesn't check anything.
223 | app = build_test_app(
224 | swagger_versions=['1.2'],
225 | **{'pyramid_swagger.exclude_paths': [r'^/sample/?']}
226 | )
227 | response = app.get(
228 | '/sample/path_arg1/resource',
229 | params={'required_arg': 'test'}
230 | )
231 | assert response.status_code == 200
232 |
233 |
234 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
235 | def test_app_error_if_path_not_in_spec_and_path_validation_disabled():
236 | """If path missing and validation is disabled we want to let something else
237 | handle the error. TestApp throws an AppError, but Pyramid would throw a
238 | HTTPNotFound exception.
239 | """
240 | with pytest.raises(AppError):
241 | app = build_test_app(
242 | swagger_versions=['1.2'],
243 | **{'pyramid_swagger.enable_path_validation': False}
244 | )
245 | assert app.get('/this/path/doesnt/exist')
246 |
247 |
248 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
249 | def test_error_handling_for_12():
250 | app = build_test_app(
251 | swagger_versions=['1.2'],
252 | **{'pyramid_swagger.enable_response_validation': True}
253 | )
254 | # Should throw 400 and not 500 (500 is thrown by pyramid_swagger when
255 | # response_validation is True and response format does not match the
256 | # type specified by the operation's swagger spec. But that match should
257 | # be done only when the response status is 200...203)
258 | assert app.get('/throw_400', expect_errors=True).status_code == 400
259 |
260 |
261 | @pytest.mark.skip(reason="Deprecated swagger 1.2 tests are broken. Skip instead of fixing.")
262 | def test_response_validation_context():
263 | request = EnhancedDummyRequest(
264 | method='GET',
265 | path='/sample/path_arg1/resource',
266 | params={'required_arg': 'test'},
267 | matchdict={'path_arg': 'path_arg1'},
268 | )
269 | # Omit the logging_info key from the response.
270 | response = Response(
271 | body=simplejson.dumps({'raw_response': 'foo'}),
272 | headers={'Content-Type': 'application/json; charset=UTF-8'},
273 | )
274 | with pytest.raises(CustomResponseValidationException):
275 | _validate_against_tween(
276 | request,
277 | response=response,
278 | **{'pyramid_swagger.validation_context_path': validation_ctx_path}
279 | )
280 |
--------------------------------------------------------------------------------
/pyramid_swagger/api.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Module for automatically serving /api-docs* via Pyramid.
4 | """
5 | from __future__ import absolute_import
6 |
7 | import copy
8 | import os.path
9 |
10 | import simplejson
11 | import yaml
12 | from bravado_core.spec import strip_xscope
13 | from six.moves.urllib.parse import urlparse
14 | from six.moves.urllib.parse import urlunparse
15 | from six.moves.urllib.request import pathname2url
16 |
17 | from pyramid_swagger.model import PyramidEndpoint
18 |
19 |
20 | # TODO: document that this is now a public interface
21 | def register_api_doc_endpoints(config, endpoints, base_path='/api-docs'):
22 | """Create and register pyramid endpoints to service swagger api docs.
23 | Routes and views will be registered on the `config` at `path`.
24 |
25 | :param config: a pyramid configuration to register the new views and routes
26 | :type config: :class:`pyramid.config.Configurator`
27 | :param endpoints: a list of endpoints to register as routes and views
28 | :type endpoints: a list of :class:`pyramid_swagger.model.PyramidEndpoint`
29 | :param base_path: the base path used to register api doc endpoints.
30 | Defaults to `/api-docs`.
31 | :type base_path: string
32 | """
33 | for endpoint in endpoints:
34 | path = base_path.rstrip('/') + endpoint.path
35 | config.add_route(endpoint.route_name, path)
36 | config.add_view(
37 | endpoint.view,
38 | route_name=endpoint.route_name,
39 | renderer=endpoint.renderer)
40 |
41 |
42 | def build_swagger_12_endpoints(resource_listing, api_declarations):
43 | """
44 | :param resource_listing: JSON representing a Swagger 1.2 resource listing
45 | :type resource_listing: dict
46 | :param api_declarations: JSON representing Swagger 1.2 api declarations
47 | :type api_declarations: dict
48 | :rtype: iterable of :class:`pyramid_swagger.model.PyramidEndpoint`
49 | """
50 | yield build_swagger_12_resource_listing(resource_listing)
51 |
52 | for name, filepath in api_declarations.items():
53 | with open(filepath) as input_file:
54 | yield build_swagger_12_api_declaration(
55 | name, simplejson.load(input_file))
56 |
57 |
58 | def build_swagger_12_resource_listing(resource_listing):
59 | """
60 | :param resource_listing: JSON representing a Swagger 1.2 resource listing
61 | :type resource_listing: dict
62 | :rtype: :class:`pyramid_swagger.model.PyramidEndpoint`
63 | """
64 | def view_for_resource_listing(request):
65 | # Thanks to the magic of closures, this means we gracefully return JSON
66 | # without file IO at request time.
67 | return resource_listing
68 |
69 | return PyramidEndpoint(
70 | path='',
71 | route_name='pyramid_swagger.swagger12.api_docs',
72 | view=view_for_resource_listing,
73 | renderer='json')
74 |
75 |
76 | def build_swagger_12_api_declaration(resource_name, api_declaration):
77 | """
78 | :param resource_name: The `path` parameter from the resource listing for
79 | this resource.
80 | :type resource_name: string
81 | :param api_declaration: JSON representing a Swagger 1.2 api declaration
82 | :type api_declaration: dict
83 | :rtype: :class:`pyramid_swagger.model.PyramidEndpoint`
84 | """
85 | # NOTE: This means our resource paths are currently constrained to be valid
86 | # pyramid routes! (minus the leading /)
87 | route_name = 'pyramid_swagger.swagger12.apidocs-{0}'.format(resource_name)
88 | return PyramidEndpoint(
89 | path='/{0}'.format(resource_name),
90 | route_name=route_name,
91 | view=build_swagger_12_api_declaration_view(api_declaration),
92 | renderer='json')
93 |
94 |
95 | def build_swagger_12_api_declaration_view(api_declaration_json):
96 | """Thanks to the magic of closures, this means we gracefully return JSON
97 | without file IO at request time.
98 | """
99 | def view_for_api_declaration(request):
100 | # Note that we rewrite basePath to always point at this server's root.
101 | return dict(
102 | api_declaration_json,
103 | basePath=str(request.application_url),
104 | )
105 | return view_for_api_declaration
106 |
107 |
108 | class NodeWalker(object):
109 | def __init__(self):
110 | pass
111 |
112 | def walk(self, item, *args, **kwargs):
113 | dupe = copy.deepcopy(item)
114 | return self._walk(dupe, *args, **kwargs)
115 |
116 | def _walk(self, item, *args, **kwargs):
117 | if isinstance(item, list):
118 | return self._walk_list(item, *args, **kwargs)
119 |
120 | elif isinstance(item, dict):
121 | return self._walk_dict(item, *args, **kwargs)
122 |
123 | else:
124 | return self._walk_item(item, *args, **kwargs)
125 |
126 | def _walk_list(self, item, *args, **kwargs):
127 | for index, subitem in enumerate(item):
128 | item[index] = self._walk(subitem, *args, **kwargs)
129 | return item
130 |
131 | def _walk_dict(self, item, *args, **kwargs):
132 | for key, value in item.items():
133 | item[key] = self._walk_dict_item(key, value, *args, **kwargs)
134 | return item
135 |
136 | def _walk_dict_item(self, key, value, *args, **kwargs):
137 | return self._walk(value, *args, **kwargs)
138 |
139 | def _walk_item(self, value, *args, **kwargs):
140 | return value
141 |
142 |
143 | def get_path_if_relative(url):
144 | parts = urlparse(url)
145 |
146 | if parts.scheme or parts.netloc:
147 | # only rewrite relative paths
148 | return
149 |
150 | if not parts.path:
151 | # don't rewrite internal refs
152 | return
153 |
154 | if parts.path.startswith('/'):
155 | # don't rewrite absolute refs
156 | return
157 |
158 | return parts
159 |
160 |
161 | class NodeWalkerForRefFiles(NodeWalker):
162 | def walk(self, spec):
163 | all_refs = []
164 |
165 | spec_fname = spec.origin_url
166 | if spec_fname.startswith('file://'):
167 | spec_fname = spec_fname.replace('file://', '')
168 | spec_dirname = os.path.dirname(spec_fname)
169 |
170 | parent = super(NodeWalkerForRefFiles, self)
171 | parent.walk(spec.client_spec_dict, spec, spec_dirname, all_refs)
172 |
173 | all_refs = [os.path.relpath(f, spec_dirname) for f in all_refs]
174 | all_refs = set(all_refs)
175 |
176 | core_dirname, core_fname = os.path.split(spec_fname)
177 | all_refs.add(core_fname)
178 |
179 | return all_refs
180 |
181 | def _walk_dict_item(self, key, value, spec, dirname, all_refs):
182 | if key != '$ref':
183 | parent = super(NodeWalkerForRefFiles, self)
184 | return parent._walk_dict_item(key, value, spec, dirname, all_refs)
185 |
186 | # assume $ref is the only key in the dict
187 | parts = get_path_if_relative(value)
188 | if not parts:
189 | return value
190 |
191 | full_fname = os.path.join(dirname, parts.path)
192 | norm_fname = os.path.normpath(full_fname)
193 | all_refs.append(norm_fname)
194 |
195 | with spec.resolver.resolving(value) as spec_dict:
196 | dupe = copy.deepcopy(spec_dict)
197 | self._walk(dupe, spec, os.path.dirname(norm_fname), all_refs)
198 |
199 |
200 | class NodeWalkerForCleaningRefs(NodeWalker):
201 | def walk(self, item, schema_format):
202 | parent = super(NodeWalkerForCleaningRefs, self)
203 | return parent.walk(item, schema_format)
204 |
205 | @staticmethod
206 | def fix_ref(ref, schema_format):
207 | parts = get_path_if_relative(ref)
208 | if not parts:
209 | return
210 |
211 | path, ext = os.path.splitext(parts.path)
212 | return urlunparse([
213 | parts.scheme,
214 | parts.netloc,
215 | '{0}.{1}'.format(path, schema_format),
216 | parts.params,
217 | parts.query,
218 | parts.fragment,
219 | ])
220 |
221 | def _walk_dict_item(self, key, value, schema_format):
222 | if key != '$ref':
223 | parent = super(NodeWalkerForCleaningRefs, self)
224 | return parent._walk_dict_item(key, value, schema_format)
225 |
226 | return self.fix_ref(value, schema_format) or value
227 |
228 |
229 | class YamlRendererFactory(object):
230 | def __init__(self, info):
231 | pass
232 |
233 | def __call__(self, value, system):
234 | response = system['request'].response
235 | response.headers['Content-Type'] = 'application/x-yaml; charset=UTF-8'
236 | return yaml.safe_dump(value).encode('utf-8')
237 |
238 |
239 | def build_swagger_20_swagger_schema_views(config):
240 | settings = config.registry.settings
241 | if settings.get('pyramid_swagger.dereference_served_schema'):
242 | views = _build_dereferenced_swagger_20_schema_views(config)
243 | else:
244 | views = _build_swagger_20_schema_views(config)
245 | return views
246 |
247 |
248 | def _build_dereferenced_swagger_20_schema_views(config):
249 | def view_for_swagger_schema(request):
250 | settings = config.registry.settings
251 | resolved_dict = settings.get('pyramid_swagger.schema20_resolved')
252 | if not resolved_dict:
253 | resolved_dict = settings['pyramid_swagger.schema20'].flattened_spec
254 | settings['pyramid_swagger.schema20_resolved'] = resolved_dict
255 | return resolved_dict
256 |
257 | for schema_format in ['yaml', 'json']:
258 | route_name = 'pyramid_swagger.swagger20.api_docs.{0}'\
259 | .format(schema_format)
260 | yield PyramidEndpoint(
261 | path='/swagger.{0}'.format(schema_format),
262 | view=view_for_swagger_schema,
263 | route_name=route_name,
264 | renderer=schema_format,
265 | )
266 |
267 |
268 | def _build_swagger_20_schema_views(config):
269 | spec = config.registry.settings['pyramid_swagger.schema20']
270 |
271 | walker = NodeWalkerForRefFiles()
272 | all_files = walker.walk(spec)
273 |
274 | file_map = {}
275 |
276 | def view_for_swagger_schema(request):
277 | _, ext = os.path.splitext(request.path)
278 | ext = ext.lstrip('.')
279 |
280 | base_path = config.registry.settings\
281 | .get('pyramid_swagger.base_path_api_docs', '').rstrip('/')
282 |
283 | key_path = request.path_info[len(base_path):]
284 |
285 | actual_fname = file_map[key_path]
286 |
287 | with spec.resolver.resolving(actual_fname) as spec_dict:
288 | clean_response = strip_xscope(spec_dict)
289 | ref_walker = NodeWalkerForCleaningRefs()
290 | fixed_spec = ref_walker.walk(clean_response, ext)
291 | return fixed_spec
292 |
293 | for ref_fname in all_files:
294 | ref_fname_parts = os.path.splitext(pathname2url(ref_fname))
295 | for schema_format in ['yaml', 'json']:
296 | route_name = 'pyramid_swagger.swagger20.api_docs.{0}.{1}'\
297 | .format(ref_fname.replace('/', '.'), schema_format)
298 | path = '/{0}.{1}'.format(ref_fname_parts[0], schema_format)
299 | file_map[path] = ref_fname
300 | yield PyramidEndpoint(
301 | path=path,
302 | route_name=route_name,
303 | view=view_for_swagger_schema,
304 | renderer=schema_format,
305 | )
306 |
--------------------------------------------------------------------------------
/tests/sample_schemas/good_app/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "Title was not specified",
5 | "version": "0.1"
6 | },
7 | "produces": ["application/json"],
8 | "paths": {
9 | "/no_models": {
10 | "get": {
11 | "responses": {
12 | "200": {
13 | "description": "No response was specified"
14 | }
15 | },
16 | "description": "",
17 | "operationId": "no_models_get"
18 | }
19 | },
20 | "/echo_date": {
21 | "post": {
22 | "parameters": [
23 | {
24 | "in": "body",
25 | "name": "body",
26 | "required": true,
27 | "schema": {
28 | "$ref": "#/definitions/object_with_formats"
29 | }
30 | }
31 | ],
32 | "responses": {
33 | "200": {
34 | "description": "HTTP/200 OK",
35 | "schema": {
36 | "$ref": "#/definitions/object_with_formats"
37 | }
38 | }
39 | },
40 | "operationId": "echo_date"
41 | }
42 | },
43 | "/post_endpoint_with_optional_body": {
44 | "post": {
45 | "description": "Returns the request body length. Used to verify that optional body parameters are well handled",
46 | "parameters": [
47 | {
48 | "in": "body",
49 | "name": "body",
50 | "required": false,
51 | "schema": {
52 | "type": "object"
53 | }
54 | }
55 | ],
56 | "responses": {
57 | "200": {
58 | "description": "HTTP/200 OK. Return request body length",
59 | "schema": {
60 | "type": "integer"
61 | }
62 | }
63 | },
64 | "operationId": "post_endpoint_with_optional_body"
65 | }
66 | },
67 | "/echo_date_json_renderer": {
68 | "post": {
69 | "description": "This endpoint is used in tests/acceptance/request_test.py and requires to be identical to /echo_date endpoint",
70 | "parameters": [
71 | {
72 | "in": "body",
73 | "name": "body",
74 | "required": true,
75 | "schema": {
76 | "$ref": "#/definitions/object_with_formats"
77 | }
78 | }
79 | ],
80 | "responses": {
81 | "200": {
82 | "description": "HTTP/200 OK",
83 | "schema": {
84 | "$ref": "#/definitions/object_with_formats"
85 | }
86 | }
87 | },
88 | "operationId": "echo_date_json_renderer"
89 | }
90 | },
91 | "/sample/{path_arg}/resource": {
92 | "get": {
93 | "responses": {
94 | "200": {
95 | "description": "Return a standard_response",
96 | "schema": {
97 | "$ref": "#/definitions/standard_response"
98 | }
99 | }
100 | },
101 | "description": "",
102 | "operationId": "standard",
103 | "parameters": [
104 | {
105 | "in": "path",
106 | "name": "path_arg",
107 | "required": true,
108 | "type": "string",
109 | "enum": ["path_arg1", "path_arg2"]
110 | },
111 | {
112 | "in": "query",
113 | "name": "required_arg",
114 | "required": true,
115 | "type": "string"
116 | },
117 | {
118 | "in": "query",
119 | "name": "optional_arg",
120 | "required": false,
121 | "type": "string"
122 | }
123 | ]
124 | }
125 | },
126 | "/sample/nonstring/{int_arg}/{float_arg}/{boolean_arg}": {
127 | "get": {
128 | "responses": {
129 | "200": {
130 | "description": "No response was specified"
131 | }
132 | },
133 | "description": "",
134 | "operationId": "sample_nonstring",
135 | "parameters": [
136 | {
137 | "in": "path",
138 | "name": "int_arg",
139 | "required": true,
140 | "type": "integer"
141 | },
142 | {
143 | "in": "path",
144 | "name": "float_arg",
145 | "required": true,
146 | "type": "number"
147 | },
148 | {
149 | "in": "path",
150 | "name": "boolean_arg",
151 | "required": true,
152 | "type": "boolean"
153 | }
154 | ]
155 | }
156 | },
157 | "/sample/header": {
158 | "get": {
159 | "responses": {
160 | "200": {
161 | "description": "No response was specified"
162 | }
163 | },
164 | "description": "",
165 | "operationId": "sample_header",
166 | "parameters": [
167 | {
168 | "in": "header",
169 | "name": "X-Force",
170 | "required": true,
171 | "type": "boolean"
172 | }
173 | ]
174 | }
175 | },
176 | "/sample/authentication": {
177 | "get": {
178 | "responses": {
179 | "200": {
180 | "description": "No response was specified"
181 | }
182 | },
183 | "description": "",
184 | "operationId": "sample_authentication",
185 | "security": [
186 | {"AuthToken": []}
187 | ]
188 | }
189 | },
190 | "/sample/no_response_schema": {
191 | "get": {
192 | "responses": {
193 | "200": {
194 | "description": "Body presence status"
195 | }
196 | },
197 | "description": "",
198 | "operationId": "sample_no_response_schema"
199 | }
200 | },
201 | "/sample": {
202 | "get": {
203 | "responses": {
204 | "200": {
205 | "description": "No response was specified"
206 | }
207 | },
208 | "description": "",
209 | "operationId": "sample_get"
210 | },
211 | "post": {
212 | "responses": {
213 | "200": {
214 | "description": "No response was specified"
215 | }
216 | },
217 | "description": "",
218 | "operationId": "sample_post",
219 | "parameters": [
220 | {
221 | "in": "query",
222 | "name": "optional_string",
223 | "required": false,
224 | "type": "string"
225 | },
226 | {
227 | "in": "body",
228 | "name": "content",
229 | "required": true,
230 | "schema": {
231 | "$ref": "#/definitions/body_model"
232 | }
233 | }
234 | ]
235 | }
236 | },
237 | "/sample_array_response": {
238 | "get": {
239 | "responses": {
240 | "200": {
241 | "description": "List of array_content_model",
242 | "schema": {
243 | "type": "array",
244 | "items": {
245 | "$ref": "#/definitions/array_content_model"
246 | }
247 | }
248 | }
249 | },
250 | "description": "",
251 | "operationId": "sample_get_array_response"
252 | }
253 | },
254 | "/post_with_primitive_body": {
255 | "post": {
256 | "responses": {
257 | "200": {
258 | "description": "No response was specified"
259 | }
260 | },
261 | "description": "",
262 | "operationId": "post_with_primitive_body",
263 | "parameters": [
264 | {
265 | "in": "body",
266 | "name": "content",
267 | "required": true,
268 | "schema": {
269 | "type": "array",
270 | "items": {
271 | "type": "string"
272 | }
273 | }
274 | }
275 | ]
276 | }
277 | },
278 | "/post_with_form_params": {
279 | "post": {
280 | "responses": {
281 | "200": {
282 | "description": "No response was specified"
283 | }
284 | },
285 | "description": "",
286 | "operationId": "post_with_form_params",
287 | "parameters": [
288 | {
289 | "in": "formData",
290 | "name": "form_param",
291 | "required": true,
292 | "type": "integer"
293 | }
294 | ]
295 | }
296 | },
297 | "/post_with_file_upload": {
298 | "post": {
299 | "responses": {
300 | "200": {
301 | "description": "Upload succeeded"
302 | }
303 | },
304 | "summary": "Test uploading a file",
305 | "operationId": "post_with_file_upload",
306 | "parameters": [
307 | {
308 | "in": "formData",
309 | "name": "photo_file",
310 | "required": true,
311 | "type": "file"
312 | }
313 | ]
314 | }
315 | },
316 | "/get_with_non_string_query_args": {
317 | "get": {
318 | "responses": {
319 | "200": {
320 | "description": "No response was specified"
321 | }
322 | },
323 | "description": "",
324 | "operationId": "get_with_non_string_query_args",
325 | "parameters": [
326 | {
327 | "in": "query",
328 | "name": "int_arg",
329 | "required": true,
330 | "type": "integer"
331 | },
332 | {
333 | "in": "query",
334 | "name": "float_arg",
335 | "required": true,
336 | "type": "number"
337 | },
338 | {
339 | "in": "query",
340 | "name": "boolean_arg",
341 | "required": true,
342 | "type": "boolean"
343 | }
344 | ]
345 | }
346 | }
347 | },
348 | "host": "localhost:9999",
349 | "schemes": [
350 | "http"
351 | ],
352 | "definitions": {
353 | "object_with_formats": {
354 | "properties": {
355 | "date": {
356 | "type": "string",
357 | "format": "date"
358 | }
359 | },
360 | "required": [
361 | "date"
362 | ],
363 | "type": "object"
364 | },
365 | "standard_response": {
366 | "type": "object",
367 | "required": [
368 | "raw_response",
369 | "logging_info"
370 | ],
371 | "additionalProperties": false,
372 | "properties": {
373 | "raw_response": {
374 | "type": "string"
375 | },
376 | "logging_info": {
377 | "type": "object"
378 | }
379 | }
380 | },
381 | "body_model": {
382 | "type": "object",
383 | "required": [
384 | "foo"
385 | ],
386 | "additionalProperties": false,
387 | "properties": {
388 | "foo": {
389 | "type": "string"
390 | },
391 | "bar": {
392 | "type": "string"
393 | }
394 | }
395 | },
396 | "array_content_model": {
397 | "required": [
398 | "enum_value"
399 | ],
400 | "properties": {
401 | "enum_value": {
402 | "type": "string",
403 | "enum": [
404 | "good_enum_value"
405 | ]
406 | }
407 | }
408 | }
409 | },
410 | "securityDefinitions": {
411 | "AuthToken": {
412 | "description": "Dummy Auth Token",
413 | "in": "header",
414 | "name": "X-Auth-Token",
415 | "type": "apiKey"
416 | }
417 | }
418 | }
419 |
--------------------------------------------------------------------------------