New upstream release.
Kali Janitor
3 years ago
2 | 2 | rev: v2.4.1 |
3 | 3 | hooks: |
4 | 4 | - id: pyupgrade |
5 | args: [--py3-plus] | |
5 | args: [--py36-plus] | |
6 | 6 | - repo: https://github.com/python/black |
7 | 7 | rev: 19.10b0 |
8 | 8 | hooks: |
62 | 62 | - Ashutosh Chaudhary `@codeasashu <https://github.com/codeasashu>`_ |
63 | 63 | - Fedor Fominykh `@fedorfo <https://github.com/fedorfo>`_ |
64 | 64 | - Colin Bounouar `@Colin-b <https://github.com/Colin-b>`_ |
65 | - Mikko Kortelainen `@kortsi <https://github.com/kortsi> | |
66 | - David Bishop `@teancom <https://github.com/teancom>`_ | |
67 | - Andrea Ghensi `@sanzoghenzo <https://github.com/sanzoghenzo>`_ | |
68 | - `@timsilvers <https://github.com/timsilvers>`_ | |
69 | - Kangwook Lee `@pbzweihander <https://github.com/pbzweihander>`_ |
0 | 0 | Changelog |
1 | 1 | --------- |
2 | ||
3 | 4.4.0 (2020-03-31) | |
4 | ****************** | |
5 | ||
6 | Features: | |
7 | ||
8 | - Populate ``additionalProperties`` from ``Meta.unknown`` (:pr:`635`). | |
9 | Thanks :user:`timsilvers` for the PR. | |
10 | - Allow ``to_yaml`` to pass kwargs to ``yaml.dump`` (:pr:`648`). | |
11 | - Resolve header references in responses (:pr:`650`). | |
12 | - Resolve example references in parameters, request bodies and responses | |
13 | (:pr:`#651`). | |
14 | ||
15 | 4.3.0 (2021-02-10) | |
16 | ****************** | |
17 | ||
18 | Features: | |
19 | ||
20 | - Add `apispec.core.Components.header` to register header components | |
21 | (:pr:`637`). | |
22 | ||
23 | 4.2.0 (2021-02-06) | |
24 | ****************** | |
25 | ||
26 | Features: | |
27 | ||
28 | - Make components public attributes of ``Components`` class (:pr:`634`). | |
29 | ||
30 | 4.1.0 (2021-01-26) | |
31 | ****************** | |
32 | ||
33 | Features: | |
34 | ||
35 | - Resolve schemas in callbacks (:pr:`544`). Thanks :user:`kortsi` for the PR. | |
36 | ||
37 | Bug fixes: | |
38 | ||
39 | - Fix docstrings documenting kwargs type as dict (:issue:`534`). | |
40 | - Use ``x-minimum`` and ``x-maximum`` extensions to document ranges that are | |
41 | not of number type (e.g. datetime) (:issue:`614`). | |
42 | ||
43 | Other changes: | |
44 | ||
45 | - Test against Python 3.9. | |
46 | ||
47 | 4.0.0 (2020-09-30) | |
48 | ****************** | |
49 | ||
50 | Features: | |
51 | ||
52 | - *Backwards-incompatible*: Automatically generate references for schemas | |
53 | passed as strings in responses and request bodies. When using | |
54 | ``MarshmallowPlugin``, if a schema is passed as string, the marshmallow | |
55 | registry is looked up for this schema name and if none is found, the name is | |
56 | assumed to be a reference to a manually created schema and a reference is | |
57 | generated. No exception is raised anymore if the schema name can't be found | |
58 | in the registry. (:pr:`554`) | |
59 | ||
60 | 4.0.0b1 (2020-09-06) | |
61 | ******************** | |
62 | ||
63 | Features: | |
64 | ||
65 | - *Backwards-incompatible*: Ignore ``location`` field metadata. This attribute | |
66 | was used in webargs but it has now been dropped. A ``Schema`` can now only | |
67 | have a single location. This simplifies the logic in ``OpenAPIConverter`` | |
68 | methods, where ``default_in`` argument now becomes ``location``. (:pr:`526`) | |
69 | - *Backwards-incompatible*: Don't document ``int`` format as ``"int32"`` and | |
70 | ``float`` format as ``"float"``, as those are platform-dependent (:pr:`595`). | |
71 | ||
72 | Refactoring: | |
73 | ||
74 | - ``OpenAPIConverter.field2parameters`` and | |
75 | ``OpenAPIConverter.property2parameter`` are removed. | |
76 | ``OpenAPIConverter.field2parameter`` becomes private. (:pr:`581`) | |
77 | ||
78 | Other changes: | |
79 | ||
80 | - Drop support for marshmallow 2. Marshmallow 3.x is required. (:pr:`583`) | |
81 | - Drop support for Python 3.5. Python 3.6+ is required. (:pr:`582`) | |
82 | ||
83 | ||
84 | 3.3.2 (2020-08-29) | |
85 | ****************** | |
86 | ||
87 | Bug fixes: | |
88 | ||
89 | - Fix crash when field metadata contains non-string keys (:pr:`596`). | |
90 | Thanks :user:`sanzoghenzo` for the fix. | |
2 | 91 | |
3 | 92 | 3.3.1 (2020-06-06) |
4 | 93 | ****************** |
8 | 97 | - Fix ``MarshmallowPlugin`` crash when ``resolve_schema_dict`` is passed a |
9 | 98 | schema as string and ``schema_name_resolver`` returns ``None`` |
10 | 99 | (:issue:`566`). Thanks :user:`black3r` for reporting and thanks |
11 | :user:`Bangterm` for the PR. | |
100 | :user:`Bangertm` for the PR. | |
12 | 101 | |
13 | 102 | 3.3.0 (2020-02-14) |
14 | 103 | ****************** |
13 | 13 | :target: https://apispec.readthedocs.io/ |
14 | 14 | :alt: Documentation |
15 | 15 | |
16 | .. image:: https://badgen.net/badge/marshmallow/2,3?list=1 | |
16 | .. image:: https://badgen.net/badge/marshmallow/3?list=1 | |
17 | 17 | :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html |
18 | :alt: marshmallow 2/3 compatible | |
18 | :alt: marshmallow 3 only | |
19 | 19 | |
20 | 20 | .. image:: https://badgen.net/badge/OAS/2,3?list=1&color=cyan |
21 | 21 | :target: https://github.com/OAI/OpenAPI-Specification |
66 | 66 | name = fields.Str() |
67 | 67 | |
68 | 68 | |
69 | # Optional security scheme support | |
70 | api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"} | |
71 | spec.components.security_scheme("ApiKeyAuth", api_key_scheme) | |
72 | ||
73 | ||
69 | 74 | # Optional Flask support |
70 | 75 | app = Flask(__name__) |
71 | 76 | |
76 | 81 | --- |
77 | 82 | get: |
78 | 83 | description: Get a random pet |
84 | security: | |
85 | - ApiKeyAuth: [] | |
79 | 86 | responses: |
80 | 87 | 200: |
81 | 88 | content: |
104 | 111 | # "/random": { |
105 | 112 | # "get": { |
106 | 113 | # "description": "Get a random pet", |
114 | # "security": [ | |
115 | # { | |
116 | # "ApiKeyAuth": [] | |
117 | # } | |
118 | # ], | |
107 | 119 | # "responses": { |
108 | 120 | # "200": { |
109 | 121 | # "content": { |
157 | 169 | # } |
158 | 170 | # } |
159 | 171 | # } |
172 | # "securitySchemes": { | |
173 | # "ApiKeyAuth": { | |
174 | # "type": "apiKey", | |
175 | # "in": "header", | |
176 | # "name": "X-API-Key" | |
177 | # } | |
178 | # } | |
160 | 179 | # } |
161 | 180 | # } |
162 | 181 | # } |
179 | 198 | # type: array |
180 | 199 | # name: {type: string} |
181 | 200 | # type: object |
201 | # securitySchemes: | |
202 | # ApiKeyAuth: | |
203 | # in: header | |
204 | # name: X-API-KEY | |
205 | # type: apiKey | |
182 | 206 | # info: {title: Swagger Petstore, version: 1.0.0} |
183 | 207 | # openapi: 3.0.2 |
184 | 208 | # paths: |
190 | 214 | # content: |
191 | 215 | # application/json: |
192 | 216 | # schema: {$ref: '#/components/schemas/Pet'} |
217 | # security: | |
218 | # - ApiKeyAuth: [] | |
193 | 219 | # tags: [] |
194 | 220 | |
195 | 221 |
26 | 26 | toxenvs: |
27 | 27 | - lint |
28 | 28 | |
29 | - py35-marshmallow2 | |
30 | - py35-marshmallow3 | |
29 | - py36-marshmallow3 | |
30 | - py37-marshmallow3 | |
31 | - py38-marshmallow3 | |
32 | - py39-marshmallow3 | |
31 | 33 | |
32 | - py36-marshmallow3 | |
33 | ||
34 | - py37-marshmallow3 | |
35 | ||
36 | - py38-marshmallow2 | |
37 | - py38-marshmallow3 | |
38 | ||
39 | - py38-marshmallowdev | |
34 | - py39-marshmallowdev | |
40 | 35 | |
41 | 36 | - docs |
42 | 37 | os: linux |
43 | 38 | - template: job--pypi-release.yml@sloria |
44 | 39 | parameters: |
45 | python: "3.8" | |
40 | python: "3.9" | |
46 | 41 | distributions: "sdist bdist_wheel" |
47 | 42 | dependsOn: |
48 | 43 | - tox_linux |
0 | apispec (4.4.0-0kali1) UNRELEASED; urgency=low | |
1 | ||
2 | * New upstream release. | |
3 | ||
4 | -- Kali Janitor <[email protected]> Tue, 20 Apr 2021 07:04:19 -0000 | |
5 | ||
0 | 6 | apispec (3.3.1-0kali2) kali-dev; urgency=medium |
1 | 7 | |
2 | 8 | [ Sophie Brun ] |
25 | 25 | source_suffix = ".rst" |
26 | 26 | master_doc = "index" |
27 | 27 | project = "apispec" |
28 | copyright = "Steven Loria {:%Y}".format(dt.datetime.utcnow()) | |
28 | copyright = f"Steven Loria {dt.datetime.utcnow():%Y}" | |
29 | 29 | |
30 | 30 | version = release = apispec.__version__ |
31 | 31 |
46 | 46 | name = fields.Str() |
47 | 47 | |
48 | 48 | |
49 | # Optional security scheme support | |
50 | api_key_scheme = {"type": "apiKey", "in": "header", "name": "X-API-Key"} | |
51 | spec.components.security_scheme("ApiKeyAuth", api_key_scheme) | |
52 | ||
53 | ||
49 | 54 | # Optional Flask support |
50 | 55 | app = Flask(__name__) |
51 | 56 | |
56 | 61 | --- |
57 | 62 | get: |
58 | 63 | description: Get a random pet |
64 | security: | |
65 | - ApiKeyAuth: [] | |
59 | 66 | responses: |
60 | 67 | 200: |
61 | 68 | description: Return a pet |
120 | 127 | # "type": "string" |
121 | 128 | # } |
122 | 129 | # } |
123 | # } | |
130 | # }, | |
131 | # } | |
132 | # }, | |
133 | # "securitySchemes": { | |
134 | # "ApiKeyAuth": { | |
135 | # "type": "apiKey", | |
136 | # "in": "header", | |
137 | # "name": "X-API-Key" | |
124 | 138 | # } |
125 | 139 | # }, |
126 | 140 | # "paths": { |
127 | 141 | # "/random": { |
128 | 142 | # "get": { |
129 | 143 | # "description": "Get a random pet", |
144 | # "security": [ | |
145 | # { | |
146 | # "ApiKeyAuth": [] | |
147 | # } | |
148 | # ], | |
130 | 149 | # "responses": { |
131 | 150 | # "200": { |
132 | 151 | # "description": "Return a pet", |
170 | 189 | # name: |
171 | 190 | # type: string |
172 | 191 | # type: object |
192 | # securitySchemes: | |
193 | # ApiKeyAuth: | |
194 | # in: header | |
195 | # name: X-API-KEY | |
196 | # type: apiKey | |
173 | 197 | # paths: |
174 | 198 | # /random: |
175 | 199 | # get: |
181 | 205 | # schema: |
182 | 206 | # $ref: '#/components/schemas/Pet' |
183 | 207 | # description: Return a pet |
208 | # security: | |
209 | # - ApiKeyAuth: [] | |
184 | 210 | |
185 | 211 | User Guide |
186 | 212 | ========== |
0 | 0 | Install |
1 | 1 | ======= |
2 | 2 | |
3 | **apispec** requires Python >= 3.5. | |
3 | **apispec** requires Python >= 3.6. | |
4 | 4 | |
5 | 5 | From the PyPI |
6 | 6 | ------------- |
24 | 24 | .. code-block:: python |
25 | 25 | |
26 | 26 | from apispec import Path, BasePlugin |
27 | from apispec.utils import load_operations_from_docstring | |
27 | from apispec.yaml_utils import load_operations_from_docstring | |
28 | 28 | |
29 | 29 | |
30 | 30 | class MyPlugin(BasePlugin): |
31 | def path_helper(self, path, func, **kwargs): | |
31 | def path_helper(self, path, operations, func, **kwargs): | |
32 | 32 | """Path helper that parses docstrings for operations. Adds a |
33 | 33 | ``func`` parameter to `apispec.APISpec.path`. |
34 | 34 | """ |
35 | operations = load_operations_from_docstring(func.__doc__) | |
36 | return Path(path=path, operations=operations) | |
35 | operations.update(load_operations_from_docstring(func.__doc__)) | |
37 | 36 | |
38 | 37 | |
39 | 38 | All plugin helpers must accept extra `**kwargs`, allowing custom plugins to define new arguments if required. |
3 | 3 | EXTRAS_REQUIRE = { |
4 | 4 | "yaml": ["PyYAML>=3.10"], |
5 | 5 | "validation": ["prance[osv]>=0.11"], |
6 | "lint": ["flake8==3.8.2", "flake8-bugbear==20.1.4", "pre-commit~=2.4"], | |
6 | "lint": ["flake8==3.9.0", "flake8-bugbear==21.3.2", "pre-commit~=2.4"], | |
7 | 7 | "docs": [ |
8 | "marshmallow>=2.19.2", | |
9 | "pyyaml==5.3.1", | |
10 | "sphinx==3.0.4", | |
8 | "marshmallow>=3.0.0", | |
9 | "pyyaml==5.4.1", | |
10 | "sphinx==3.5.3", | |
11 | 11 | "sphinx-issues==1.2.0", |
12 | "sphinx-rtd-theme==0.4.3", | |
12 | "sphinx-rtd-theme==0.5.1", | |
13 | 13 | ], |
14 | 14 | } |
15 | 15 | EXTRAS_REQUIRE["tests"] = ( |
16 | 16 | EXTRAS_REQUIRE["yaml"] |
17 | 17 | + EXTRAS_REQUIRE["validation"] |
18 | + ["marshmallow>=2.19.2", "pytest", "mock"] | |
18 | + ["marshmallow>=3.0.0", "pytest", "mock"] | |
19 | 19 | ) |
20 | 20 | EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"] |
21 | 21 | |
59 | 59 | license="MIT", |
60 | 60 | zip_safe=False, |
61 | 61 | keywords="apispec swagger openapi specification oas documentation spec rest api", |
62 | python_requires=">=3.5", | |
62 | python_requires=">=3.6", | |
63 | 63 | classifiers=[ |
64 | 64 | "License :: OSI Approved :: MIT License", |
65 | 65 | "Programming Language :: Python :: 3", |
66 | "Programming Language :: Python :: 3.5", | |
67 | 66 | "Programming Language :: Python :: 3.6", |
68 | 67 | "Programming Language :: Python :: 3.7", |
69 | 68 | "Programming Language :: Python :: 3.8", |
69 | "Programming Language :: Python :: 3.9", | |
70 | 70 | "Programming Language :: Python :: 3 :: Only", |
71 | 71 | ], |
72 | 72 | test_suite="tests", |
2 | 2 | from .core import APISpec |
3 | 3 | from .plugin import BasePlugin |
4 | 4 | |
5 | __version__ = "3.3.1" | |
5 | __version__ = "4.4.0" | |
6 | 6 | __all__ = ["APISpec", "BasePlugin"] |
28 | 28 | def __init__(self, plugins, openapi_version): |
29 | 29 | self._plugins = plugins |
30 | 30 | self.openapi_version = openapi_version |
31 | self._schemas = {} | |
32 | self._responses = {} | |
33 | self._parameters = {} | |
34 | self._examples = {} | |
35 | self._security_schemes = {} | |
31 | self.schemas = {} | |
32 | self.responses = {} | |
33 | self.parameters = {} | |
34 | self.headers = {} | |
35 | self.examples = {} | |
36 | self.security_schemes = {} | |
36 | 37 | |
37 | 38 | def to_dict(self): |
38 | 39 | subsections = { |
39 | "schema": self._schemas, | |
40 | "response": self._responses, | |
41 | "parameter": self._parameters, | |
42 | "example": self._examples, | |
43 | "security_scheme": self._security_schemes, | |
40 | "schema": self.schemas, | |
41 | "response": self.responses, | |
42 | "parameter": self.parameters, | |
43 | "header": self.headers, | |
44 | "example": self.examples, | |
45 | "security_scheme": self.security_schemes, | |
44 | 46 | } |
45 | 47 | return { |
46 | 48 | COMPONENT_SUBSECTIONS[self.openapi_version.major][k]: v |
69 | 71 | |
70 | 72 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schemaObject |
71 | 73 | """ |
72 | if name in self._schemas: | |
73 | raise DuplicateComponentNameError( | |
74 | 'Another schema with name "{}" is already registered.'.format(name) | |
74 | if name in self.schemas: | |
75 | raise DuplicateComponentNameError( | |
76 | f'Another schema with name "{name}" is already registered.' | |
75 | 77 | ) |
76 | 78 | component = component or {} |
77 | 79 | ret = component.copy() |
81 | 83 | ret.update(plugin.schema_helper(name, component, **kwargs) or {}) |
82 | 84 | except PluginMethodNotImplementedError: |
83 | 85 | continue |
84 | self._schemas[name] = ret | |
86 | self.schemas[name] = ret | |
85 | 87 | return self |
86 | 88 | |
87 | 89 | def response(self, component_id, component=None, **kwargs): |
89 | 91 | |
90 | 92 | :param str component_id: ref_id to use as reference |
91 | 93 | :param dict component: response fields |
92 | :param dict kwargs: plugin-specific arguments | |
93 | """ | |
94 | if component_id in self._responses: | |
94 | :param kwargs: plugin-specific arguments | |
95 | """ | |
96 | if component_id in self.responses: | |
95 | 97 | raise DuplicateComponentNameError( |
96 | 98 | 'Another response with name "{}" is already registered.'.format( |
97 | 99 | component_id |
105 | 107 | ret.update(plugin.response_helper(component, **kwargs) or {}) |
106 | 108 | except PluginMethodNotImplementedError: |
107 | 109 | continue |
108 | self._responses[component_id] = ret | |
110 | self.responses[component_id] = ret | |
109 | 111 | return self |
110 | 112 | |
111 | 113 | def parameter(self, component_id, location, component=None, **kwargs): |
112 | 114 | """ Add a parameter which can be referenced. |
113 | 115 | |
114 | :param str param_id: identifier by which parameter may be referenced. | |
116 | :param str component_id: identifier by which parameter may be referenced. | |
115 | 117 | :param str location: location of the parameter. |
116 | 118 | :param dict component: parameter fields. |
117 | :param dict kwargs: plugin-specific arguments | |
118 | """ | |
119 | if component_id in self._parameters: | |
119 | :param kwargs: plugin-specific arguments | |
120 | """ | |
121 | if component_id in self.parameters: | |
120 | 122 | raise DuplicateComponentNameError( |
121 | 123 | 'Another parameter with name "{}" is already registered.'.format( |
122 | 124 | component_id |
137 | 139 | ret.update(plugin.parameter_helper(component, **kwargs) or {}) |
138 | 140 | except PluginMethodNotImplementedError: |
139 | 141 | continue |
140 | self._parameters[component_id] = ret | |
142 | self.parameters[component_id] = ret | |
143 | return self | |
144 | ||
145 | def header(self, component_id, component): | |
146 | """ Add a header which can be referenced. | |
147 | ||
148 | :param str component_id: identifier by which header may be referenced. | |
149 | :param dict component: header fields. | |
150 | ||
151 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#headerObject | |
152 | """ | |
153 | if component_id in self.headers: | |
154 | raise DuplicateComponentNameError( | |
155 | f'Another header with name "{component_id}" is already registered.' | |
156 | ) | |
157 | self.headers[component_id] = component | |
141 | 158 | return self |
142 | 159 | |
143 | 160 | def example(self, name, component, **kwargs): |
148 | 165 | |
149 | 166 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#exampleObject |
150 | 167 | """ |
151 | if name in self._examples: | |
152 | raise DuplicateComponentNameError( | |
153 | 'Another example with name "{}" is already registered.'.format(name) | |
154 | ) | |
155 | self._examples[name] = component | |
168 | if name in self.examples: | |
169 | raise DuplicateComponentNameError( | |
170 | f'Another example with name "{name}" is already registered.' | |
171 | ) | |
172 | self.examples[name] = component | |
156 | 173 | return self |
157 | 174 | |
158 | 175 | def security_scheme(self, component_id, component): |
159 | 176 | """Add a security scheme which can be referenced. |
160 | 177 | |
161 | 178 | :param str component_id: component_id to use as reference |
162 | :param dict kwargs: security scheme fields | |
163 | """ | |
164 | if component_id in self._security_schemes: | |
179 | :param dict component: security scheme fields | |
180 | """ | |
181 | if component_id in self.security_schemes: | |
165 | 182 | raise DuplicateComponentNameError( |
166 | 183 | 'Another security scheme with name "{}" is already registered.'.format( |
167 | 184 | component_id |
168 | 185 | ) |
169 | 186 | ) |
170 | self._security_schemes[component_id] = component | |
187 | self.security_schemes[component_id] = component | |
171 | 188 | return self |
172 | 189 | |
173 | 190 | |
180 | 197 | See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject |
181 | 198 | :param str|OpenAPIVersion openapi_version: OpenAPI Specification version. |
182 | 199 | Should be in the form '2.x' or '3.x.x' to comply with the OpenAPI standard. |
183 | :param dict options: Optional top-level keys | |
200 | :param options: Optional top-level keys | |
184 | 201 | See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#openapi-object |
185 | 202 | """ |
186 | 203 | |
220 | 237 | ret = deepupdate(ret, self.options) |
221 | 238 | return ret |
222 | 239 | |
223 | def to_yaml(self): | |
224 | """Render the spec to YAML. Requires PyYAML to be installed.""" | |
240 | def to_yaml(self, yaml_dump_kwargs=None): | |
241 | """Render the spec to YAML. Requires PyYAML to be installed. | |
242 | ||
243 | :param dict yaml_dump_kwargs: Additional keyword arguments to pass to `yaml.dump` | |
244 | """ | |
225 | 245 | from .yaml_utils import dict_to_yaml |
226 | 246 | |
227 | return dict_to_yaml(self.to_dict()) | |
247 | return dict_to_yaml(self.to_dict(), yaml_dump_kwargs) | |
228 | 248 | |
229 | 249 | def tag(self, tag): |
230 | 250 | """ Store information about a tag. |
242 | 262 | summary=None, |
243 | 263 | description=None, |
244 | 264 | parameters=None, |
245 | **kwargs | |
265 | **kwargs, | |
246 | 266 | ): |
247 | 267 | """Add a new path object to the spec. |
248 | 268 | |
253 | 273 | :param str summary: short summary relevant to all operations in this path |
254 | 274 | :param str description: long description relevant to all operations in this path |
255 | 275 | :param list|None parameters: list of parameters relevant to all operations in this path |
256 | :param dict kwargs: parameters used by any path helpers see :meth:`register_path_helper` | |
276 | :param kwargs: parameters used by any path helpers see :meth:`register_path_helper` | |
257 | 277 | """ |
258 | 278 | # operations and parameters must be deepcopied because they are mutated |
259 | 279 | # in clean_operations and operation helpers and path may be called twice |
299 | 319 | Otherwise, it is assumed to be a reference name as string and the corresponding $ref |
300 | 320 | string is returned. |
301 | 321 | |
302 | :param str obj_type: "parameter" or "response" | |
303 | :param dict|str obj: parameter or response in dict form or as ref_id string | |
322 | :param str obj_type: "schema", "parameter", "response" or "security_scheme" | |
323 | :param dict|str obj: object in dict form or as ref_id string | |
304 | 324 | """ |
305 | 325 | if isinstance(obj, dict): |
306 | 326 | return obj |
307 | 327 | return build_reference(obj_type, self.openapi_version.major, obj) |
328 | ||
329 | def _resolve_schema(self, obj): | |
330 | """Replace schema reference as string with a $ref if needed.""" | |
331 | if not isinstance(obj, dict): | |
332 | return | |
333 | if self.openapi_version.major < 3: | |
334 | if "schema" in obj: | |
335 | obj["schema"] = self.get_ref("schema", obj["schema"]) | |
336 | else: | |
337 | if "content" in obj: | |
338 | for content in obj["content"].values(): | |
339 | if "schema" in content: | |
340 | content["schema"] = self.get_ref("schema", content["schema"]) | |
341 | ||
342 | def _resolve_examples(self, obj): | |
343 | """Replace example reference as string with a $ref""" | |
344 | for name, example in obj.get("examples", {}).items(): | |
345 | obj["examples"][name] = self.get_ref("example", example) | |
308 | 346 | |
309 | 347 | def clean_parameters(self, parameters): |
310 | 348 | """Ensure that all parameters with "in" equal to "path" are also required |
322 | 360 | missing_attrs = [attr for attr in ("name", "in") if attr not in parameter] |
323 | 361 | if missing_attrs: |
324 | 362 | raise InvalidParameterError( |
325 | "Missing keys {} for parameter".format(missing_attrs) | |
363 | f"Missing keys {missing_attrs} for parameter" | |
326 | 364 | ) |
327 | 365 | |
328 | 366 | # OpenAPI Spec 3 and 2 don't allow for duplicated parameters |
340 | 378 | if parameter["in"] == "path": |
341 | 379 | parameter["required"] = True |
342 | 380 | |
381 | self._resolve_examples(parameter) | |
382 | ||
343 | 383 | return [self.get_ref("parameter", p) for p in parameters] |
344 | 384 | |
345 | 385 | def clean_operations(self, operations): |
364 | 404 | for operation in (operations or {}).values(): |
365 | 405 | if "parameters" in operation: |
366 | 406 | operation["parameters"] = self.clean_parameters(operation["parameters"]) |
407 | # OAS 3 | |
408 | if "requestBody" in operation: | |
409 | self._resolve_schema(operation["requestBody"]) | |
410 | for media_type in operation["requestBody"]["content"].values(): | |
411 | self._resolve_examples(media_type) | |
412 | ||
367 | 413 | if "responses" in operation: |
368 | 414 | responses = OrderedDict() |
369 | 415 | for code, response in operation["responses"].items(): |
372 | 418 | except (TypeError, ValueError): |
373 | 419 | if self.openapi_version.major < 3 and code != "default": |
374 | 420 | warnings.warn("Non-integer code not allowed in OpenAPI < 3") |
375 | ||
376 | responses[str(code)] = self.get_ref("response", response) | |
421 | self._resolve_schema(response) | |
422 | response = self.get_ref("response", response) | |
423 | for name, header in response.get("headers", {}).items(): | |
424 | response["headers"][name] = self.get_ref("header", header) | |
425 | for media_type in response.get("content", {}).values(): | |
426 | self._resolve_examples(media_type) | |
427 | responses[str(code)] = response | |
377 | 428 | operation["responses"] = responses |
59 | 59 | # 'format': 'date-time', |
60 | 60 | # 'readOnly': True, |
61 | 61 | # 'type': 'string'}, |
62 | # 'id': {'format': 'int32', | |
63 | # 'readOnly': True, | |
62 | # 'id': {'readOnly': True, | |
64 | 63 | # 'type': 'integer'}, |
65 | 64 | # 'name': {'description': "The user's name", |
66 | 65 | # 'type': 'string'}}, |
139 | 138 | class MyCustomField(Integer): |
140 | 139 | # ... |
141 | 140 | |
142 | @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', 'int32') | |
141 | @ma_plugin.map_to_openapi_type(Integer) # will map to ('integer', None) | |
143 | 142 | class MyCustomFieldThatsKindaLikeAnInteger(Integer): |
144 | 143 | # ... |
145 | 144 | """ |
187 | 186 | return response |
188 | 187 | |
189 | 188 | def operation_helper(self, operations, **kwargs): |
190 | for operation in operations.values(): | |
191 | if not isinstance(operation, dict): | |
192 | continue | |
193 | if "parameters" in operation: | |
194 | operation["parameters"] = self.resolver.resolve_parameters( | |
195 | operation["parameters"] | |
196 | ) | |
197 | if self.openapi_version.major >= 3: | |
198 | if "requestBody" in operation: | |
199 | self.resolver.resolve_schema(operation["requestBody"]) | |
200 | for response in operation.get("responses", {}).values(): | |
201 | self.resolver.resolve_response(response) | |
189 | self.resolver.resolve_operations(operations) | |
202 | 190 | |
203 | 191 | def warn_if_schema_already_in_spec(self, schema_key): |
204 | 192 | """Method to warn the user if the schema has already been added to the |
19 | 19 | return schema() |
20 | 20 | if isinstance(schema, marshmallow.Schema): |
21 | 21 | return schema |
22 | try: | |
23 | return marshmallow.class_registry.get_class(schema)() | |
24 | except marshmallow.exceptions.RegistryError: | |
25 | raise ValueError( | |
26 | "{!r} is not a marshmallow.Schema subclass or instance and has not" | |
27 | " been registered in the marshmallow class registry.".format(schema) | |
28 | ) | |
22 | return marshmallow.class_registry.get_class(schema)() | |
29 | 23 | |
30 | 24 | |
31 | 25 | def resolve_schema_cls(schema): |
38 | 32 | return schema |
39 | 33 | if isinstance(schema, marshmallow.Schema): |
40 | 34 | return type(schema) |
41 | try: | |
42 | return marshmallow.class_registry.get_class(schema) | |
43 | except marshmallow.exceptions.RegistryError: | |
44 | raise ValueError( | |
45 | "{!r} is not a marshmallow.Schema subclass or instance and has not" | |
46 | " been registered in the marshmallow class registry.".format(schema) | |
47 | ) | |
35 | return marshmallow.class_registry.get_class(schema) | |
48 | 36 | |
49 | 37 | |
50 | 38 | def get_fields(schema, *, exclude_dump_only=False): |
60 | 48 | fields = copy.deepcopy(schema._declared_fields) |
61 | 49 | else: |
62 | 50 | raise ValueError( |
63 | "{!r} doesn't have either `fields` or `_declared_fields`.".format(schema) | |
51 | f"{schema!r} doesn't have either `fields` or `_declared_fields`." | |
64 | 52 | ) |
65 | 53 | Meta = getattr(schema, "Meta", None) |
66 | 54 | warn_if_fields_defined_in_meta(fields, Meta) |
90 | 78 | |
91 | 79 | :param dict fields: A dictionary of fields name field object pairs |
92 | 80 | :param Meta: the schema's Meta class |
93 | :param bool exclude_dump_only: whether to filter fields in Meta.dump_only | |
81 | :param bool exclude_dump_only: whether to filter dump_only fields | |
94 | 82 | """ |
95 | 83 | exclude = list(getattr(Meta, "exclude", [])) |
96 | 84 | if exclude_dump_only: |
97 | 85 | exclude.extend(getattr(Meta, "dump_only", [])) |
98 | 86 | |
99 | 87 | filtered_fields = OrderedDict( |
100 | (key, value) for key, value in fields.items() if key not in exclude | |
88 | (key, value) | |
89 | for key, value in fields.items() | |
90 | if key not in exclude and not (exclude_dump_only and value.dump_only) | |
101 | 91 | ) |
102 | 92 | |
103 | 93 | return filtered_fields |
132 | 122 | :param int counter: the counter of the number of recursions |
133 | 123 | :return: the unique name |
134 | 124 | """ |
135 | if name not in components._schemas: | |
125 | if name not in components.schemas: | |
136 | 126 | return name |
137 | 127 | if not counter: # first time through recursion |
138 | 128 | warnings.warn( |
16 | 16 | |
17 | 17 | RegexType = type(re.compile("")) |
18 | 18 | |
19 | MARSHMALLOW_VERSION_INFO = tuple( | |
20 | [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()] | |
21 | ) | |
22 | ||
23 | ||
24 | 19 | # marshmallow field => (JSON Schema type, format) |
25 | 20 | DEFAULT_FIELD_MAPPING = { |
26 | marshmallow.fields.Integer: ("integer", "int32"), | |
21 | marshmallow.fields.Integer: ("integer", None), | |
27 | 22 | marshmallow.fields.Number: ("number", None), |
28 | marshmallow.fields.Float: ("number", "float"), | |
23 | marshmallow.fields.Float: ("number", None), | |
29 | 24 | marshmallow.fields.Decimal: ("number", None), |
30 | 25 | marshmallow.fields.String: ("string", None), |
31 | 26 | marshmallow.fields.Boolean: ("boolean", None), |
87 | 82 | |
88 | 83 | def init_attribute_functions(self): |
89 | 84 | self.attribute_functions = [ |
85 | # self.field2type_and_format should run first | |
86 | # as other functions may rely on its output | |
90 | 87 | self.field2type_and_format, |
91 | 88 | self.field2default, |
92 | 89 | self.field2choices, |
211 | 208 | else: |
212 | 209 | default = field.missing |
213 | 210 | if default is not marshmallow.missing and not callable(default): |
214 | if MARSHMALLOW_VERSION_INFO[0] >= 3: | |
215 | default = field._serialize(default, None, None) | |
211 | default = field._serialize(default, None, None) | |
216 | 212 | ret["default"] = default |
217 | 213 | return ret |
218 | 214 | |
277 | 273 | ] = True |
278 | 274 | return attributes |
279 | 275 | |
280 | def field2range(self, field, **kwargs): | |
276 | def field2range(self, field, ret): | |
281 | 277 | """Return the dictionary of OpenAPI field attributes for a set of |
282 | 278 | :class:`Range <marshmallow.validators.Range>` validators. |
283 | 279 | |
294 | 290 | ) |
295 | 291 | ] |
296 | 292 | |
297 | attributes = {} | |
298 | for validator in validators: | |
299 | if validator.min is not None: | |
300 | if hasattr(attributes, "minimum"): | |
301 | attributes["minimum"] = max(attributes["minimum"], validator.min) | |
302 | else: | |
303 | attributes["minimum"] = validator.min | |
304 | if validator.max is not None: | |
305 | if hasattr(attributes, "maximum"): | |
306 | attributes["maximum"] = min(attributes["maximum"], validator.max) | |
307 | else: | |
308 | attributes["maximum"] = validator.max | |
309 | return attributes | |
293 | min_attr, max_attr = ( | |
294 | ("minimum", "maximum") | |
295 | if ret.get("type") in {"number", "integer"} | |
296 | else ("x-minimum", "x-maximum") | |
297 | ) | |
298 | return make_min_max_attributes(validators, min_attr, max_attr) | |
310 | 299 | |
311 | 300 | def field2length(self, field, **kwargs): |
312 | 301 | """Return the dictionary of OpenAPI field attributes for a set of |
315 | 304 | :param Field field: A marshmallow field. |
316 | 305 | :rtype: dict |
317 | 306 | """ |
318 | attributes = {} | |
319 | ||
320 | 307 | validators = [ |
321 | 308 | validator |
322 | 309 | for validator in field.validators |
333 | 320 | min_attr = "minItems" if is_array else "minLength" |
334 | 321 | max_attr = "maxItems" if is_array else "maxLength" |
335 | 322 | |
336 | for validator in validators: | |
337 | if validator.min is not None: | |
338 | if hasattr(attributes, min_attr): | |
339 | attributes[min_attr] = max(attributes[min_attr], validator.min) | |
340 | else: | |
341 | attributes[min_attr] = validator.min | |
342 | if validator.max is not None: | |
343 | if hasattr(attributes, max_attr): | |
344 | attributes[max_attr] = min(attributes[max_attr], validator.max) | |
345 | else: | |
346 | attributes[max_attr] = validator.max | |
347 | ||
348 | for validator in validators: | |
349 | if validator.equal is not None: | |
350 | attributes[min_attr] = validator.equal | |
351 | attributes[max_attr] = validator.equal | |
352 | return attributes | |
323 | equal_list = [ | |
324 | validator.equal for validator in validators if validator.equal is not None | |
325 | ] | |
326 | if equal_list: | |
327 | return {min_attr: equal_list[0], max_attr: equal_list[0]} | |
328 | ||
329 | return make_min_max_attributes(validators, min_attr, max_attr) | |
353 | 330 | |
354 | 331 | def field2pattern(self, field, **kwargs): |
355 | 332 | """Return the dictionary of OpenAPI field attributes for a set of |
396 | 373 | metadata = { |
397 | 374 | key.replace("_", "-") if key.startswith("x_") else key: value |
398 | 375 | for key, value in field.metadata.items() |
376 | if isinstance(key, str) | |
399 | 377 | } |
400 | 378 | |
401 | 379 | # Avoid validation error with "Additional properties not allowed" |
435 | 413 | """ |
436 | 414 | ret = {} |
437 | 415 | if isinstance(field, marshmallow.fields.List): |
438 | inner_field = ( | |
439 | field.inner if MARSHMALLOW_VERSION_INFO[0] >= 3 else field.container | |
440 | ) | |
441 | ret["items"] = self.field2property(inner_field) | |
416 | ret["items"] = self.field2property(field.inner) | |
442 | 417 | return ret |
443 | 418 | |
444 | 419 | def dict2properties(self, field, **kwargs): |
452 | 427 | """ |
453 | 428 | ret = {} |
454 | 429 | if isinstance(field, marshmallow.fields.Dict): |
455 | if MARSHMALLOW_VERSION_INFO[0] >= 3: | |
456 | value_field = field.value_field | |
457 | if value_field: | |
458 | ret["additionalProperties"] = self.field2property(value_field) | |
459 | return ret | |
430 | value_field = field.value_field | |
431 | if value_field: | |
432 | ret["additionalProperties"] = self.field2property(value_field) | |
433 | return ret | |
434 | ||
435 | ||
436 | def make_min_max_attributes(validators, min_attr, max_attr): | |
437 | """Return a dictionary of minimum and maximum attributes based on a list | |
438 | of validators. If either minimum or maximum values are not present in any | |
439 | of the validator objects that attribute will be omitted. | |
440 | ||
441 | :param validators list: A list of `Marshmallow` validator objects. Each | |
442 | objct is inspected for a minimum and maximum values | |
443 | :param min_attr string: The OpenAPI attribute for the minimum value | |
444 | :param max_attr string: The OpenAPI attribute for the maximum value | |
445 | """ | |
446 | attributes = {} | |
447 | min_list = [validator.min for validator in validators if validator.min is not None] | |
448 | max_list = [validator.max for validator in validators if validator.max is not None] | |
449 | if min_list: | |
450 | attributes[min_attr] = max(min_list) | |
451 | if max_list: | |
452 | attributes[max_attr] = min(max_list) | |
453 | return attributes |
18 | 18 | make_schema_key, |
19 | 19 | resolve_schema_instance, |
20 | 20 | get_unique_schema_name, |
21 | ) | |
22 | ||
23 | ||
24 | MARSHMALLOW_VERSION_INFO = tuple( | |
25 | [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()] | |
26 | 21 | ) |
27 | 22 | |
28 | 23 | |
59 | 54 | # Schema references |
60 | 55 | self.refs = {} |
61 | 56 | |
62 | @staticmethod | |
63 | def _observed_name(field, name): | |
64 | """Adjust field name to reflect `dump_to` and `load_from` attributes. | |
65 | ||
66 | :param Field field: A marshmallow field. | |
67 | :param str name: Field name | |
68 | :rtype: str | |
69 | """ | |
70 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
71 | # use getattr in case we're running against older versions of marshmallow. | |
72 | dump_to = getattr(field, "dump_to", None) | |
73 | load_from = getattr(field, "load_from", None) | |
74 | return dump_to or load_from or name | |
75 | return field.data_key or name | |
76 | ||
77 | 57 | def resolve_nested_schema(self, schema): |
78 | 58 | """Return the OpenAPI representation of a marshmallow Schema. |
79 | 59 | |
86 | 66 | |
87 | 67 | :param schema: schema to add to the spec |
88 | 68 | """ |
89 | schema_instance = resolve_schema_instance(schema) | |
69 | try: | |
70 | schema_instance = resolve_schema_instance(schema) | |
71 | # If schema is a string and is not found in registry, | |
72 | # assume it is a schema reference | |
73 | except marshmallow.exceptions.RegistryError: | |
74 | return build_reference("schema", self.openapi_version.major, schema) | |
90 | 75 | schema_key = make_schema_key(schema_instance) |
91 | 76 | if schema_key not in self.refs: |
92 | 77 | name = self.schema_name_resolver(schema) |
109 | 94 | return self.get_ref_dict(schema_instance) |
110 | 95 | |
111 | 96 | def schema2parameters( |
112 | self, | |
113 | schema, | |
114 | *, | |
115 | default_in="body", | |
116 | name="body", | |
117 | required=False, | |
118 | description=None | |
97 | self, schema, *, location, name="body", required=False, description=None | |
119 | 98 | ): |
120 | 99 | """Return an array of OpenAPI parameters given a given marshmallow |
121 | :class:`Schema <marshmallow.Schema>`. If `default_in` is "body", then return an array | |
100 | :class:`Schema <marshmallow.Schema>`. If `location` is "body", then return an array | |
122 | 101 | of a single parameter; else return an array of a parameter for each included field in |
123 | 102 | the :class:`Schema <marshmallow.Schema>`. |
124 | 103 | |
104 | In OpenAPI 3, only "query", "header", "path" or "cookie" are allowed for the location | |
105 | of parameters. "requestBody" is used when fields are in the body. | |
106 | ||
125 | 107 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject |
126 | 108 | """ |
127 | openapi_default_in = __location_map__.get(default_in, default_in) | |
128 | if self.openapi_version.major < 3 and openapi_default_in == "body": | |
129 | prop = self.resolve_nested_schema(schema) | |
130 | ||
109 | location = __location_map__.get(location, location) | |
110 | # OAS 2 body parameter | |
111 | if location == "body": | |
131 | 112 | param = { |
132 | "in": openapi_default_in, | |
113 | "in": location, | |
133 | 114 | "required": required, |
134 | 115 | "name": name, |
135 | "schema": prop, | |
116 | "schema": self.resolve_nested_schema(schema), | |
136 | 117 | } |
137 | ||
138 | 118 | if description: |
139 | 119 | param["description"] = description |
140 | ||
141 | 120 | return [param] |
142 | 121 | |
143 | 122 | assert not getattr( |
146 | 125 | |
147 | 126 | fields = get_fields(schema, exclude_dump_only=True) |
148 | 127 | |
149 | return self.fields2parameters(fields, default_in=default_in) | |
150 | ||
151 | def fields2parameters(self, fields, *, default_in): | |
152 | """Return an array of OpenAPI parameters given a mapping between field names and | |
153 | :class:`Field <marshmallow.Field>` objects. If `default_in` is "body", then return an array | |
154 | of a single parameter; else return an array of a parameter for each included field in | |
155 | the :class:`Schema <marshmallow.Schema>`. | |
156 | ||
157 | In OpenAPI3, only "query", "header", "path" or "cookie" are allowed for the location | |
158 | of parameters. In OpenAPI 3, "requestBody" is used when fields are in the body. | |
159 | ||
160 | This function always returns a list, with a parameter | |
161 | for each included field in the :class:`Schema <marshmallow.Schema>`. | |
162 | ||
163 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject | |
164 | """ | |
165 | parameters = [] | |
166 | body_param = None | |
167 | for field_name, field_obj in fields.items(): | |
168 | if field_obj.dump_only: | |
169 | continue | |
170 | param = self.field2parameter( | |
171 | field_obj, | |
172 | name=self._observed_name(field_obj, field_name), | |
173 | default_in=default_in, | |
128 | return [ | |
129 | self._field2parameter( | |
130 | field_obj, name=field_obj.data_key or field_name, location=location, | |
174 | 131 | ) |
175 | if ( | |
176 | self.openapi_version.major < 3 | |
177 | and param["in"] == "body" | |
178 | and body_param is not None | |
179 | ): | |
180 | body_param["schema"]["properties"].update(param["schema"]["properties"]) | |
181 | required_fields = param["schema"].get("required", []) | |
182 | if required_fields: | |
183 | body_param["schema"].setdefault("required", []).extend( | |
184 | required_fields | |
185 | ) | |
186 | else: | |
187 | if self.openapi_version.major < 3 and param["in"] == "body": | |
188 | body_param = param | |
189 | parameters.append(param) | |
190 | return parameters | |
191 | ||
192 | def field2parameter(self, field, *, name, default_in): | |
132 | for field_name, field_obj in fields.items() | |
133 | ] | |
134 | ||
135 | def _field2parameter(self, field, *, name, location): | |
193 | 136 | """Return an OpenAPI parameter as a `dict`, given a marshmallow |
194 | 137 | :class:`Field <marshmallow.Field>`. |
195 | 138 | |
196 | 139 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject |
197 | 140 | """ |
198 | location = field.metadata.get("location", None) | |
141 | ret = {"in": location, "name": name, "required": field.required} | |
142 | ||
199 | 143 | prop = self.field2property(field) |
200 | return self.property2parameter( | |
201 | prop, | |
202 | name=name, | |
203 | required=field.required, | |
204 | multiple=isinstance(field, marshmallow.fields.List), | |
205 | location=location, | |
206 | default_in=default_in, | |
207 | ) | |
208 | ||
209 | def property2parameter( | |
210 | self, prop, *, name, required, multiple, location, default_in | |
211 | ): | |
212 | """Return the Parameter Object definition for a JSON Schema property. | |
213 | ||
214 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameterObject | |
215 | ||
216 | :param dict prop: JSON Schema property | |
217 | :param str name: Field name | |
218 | :param bool required: Parameter is required | |
219 | :param bool multiple: Parameter is repeated | |
220 | :param str location: Location to look for ``name`` | |
221 | :param str default_in: Default location to look for ``name`` | |
222 | :raise: TranslationError if arg object cannot be translated to a Parameter Object schema. | |
223 | :rtype: dict, a Parameter Object | |
224 | """ | |
225 | openapi_default_in = __location_map__.get(default_in, default_in) | |
226 | openapi_location = __location_map__.get(location, openapi_default_in) | |
227 | ret = {"in": openapi_location, "name": name} | |
228 | ||
229 | if openapi_location == "body": | |
230 | ret["required"] = False | |
231 | ret["name"] = "body" | |
232 | ret["schema"] = {"type": "object", "properties": {name: prop}} | |
233 | if required: | |
234 | ret["schema"]["required"] = [name] | |
144 | multiple = isinstance(field, marshmallow.fields.List) | |
145 | ||
146 | if self.openapi_version.major < 3: | |
147 | if multiple: | |
148 | ret["collectionFormat"] = "multi" | |
149 | ret.update(prop) | |
235 | 150 | else: |
236 | ret["required"] = required | |
237 | if self.openapi_version.major < 3: | |
238 | if multiple: | |
239 | ret["collectionFormat"] = "multi" | |
240 | ret.update(prop) | |
241 | else: | |
242 | if multiple: | |
243 | ret["explode"] = True | |
244 | ret["style"] = "form" | |
245 | if prop.get("description", None): | |
246 | ret["description"] = prop.pop("description") | |
247 | ret["schema"] = prop | |
151 | if multiple: | |
152 | ret["explode"] = True | |
153 | ret["style"] = "form" | |
154 | if prop.get("description", None): | |
155 | ret["description"] = prop.pop("description") | |
156 | ret["schema"] = prop | |
248 | 157 | return ret |
249 | 158 | |
250 | 159 | def schema2jsonschema(self, schema): |
268 | 177 | jsonschema["title"] = Meta.title |
269 | 178 | if hasattr(Meta, "description"): |
270 | 179 | jsonschema["description"] = Meta.description |
180 | if hasattr(Meta, "unknown"): | |
181 | jsonschema["additionalProperties"] = Meta.unknown == marshmallow.INCLUDE | |
271 | 182 | |
272 | 183 | return jsonschema |
273 | 184 | |
285 | 196 | jsonschema = {"type": "object", "properties": OrderedDict() if ordered else {}} |
286 | 197 | |
287 | 198 | for field_name, field_obj in fields.items(): |
288 | observed_field_name = self._observed_name(field_obj, field_name) | |
289 | property = self.field2property(field_obj) | |
290 | jsonschema["properties"][observed_field_name] = property | |
199 | observed_field_name = field_obj.data_key or field_name | |
200 | prop = self.field2property(field_obj) | |
201 | jsonschema["properties"][observed_field_name] = prop | |
291 | 202 | |
292 | 203 | if field_obj.required: |
293 | 204 | if not partial or ( |
14 | 14 | self.openapi_version = openapi_version |
15 | 15 | self.converter = converter |
16 | 16 | |
17 | def resolve_operations(self, operations, **kwargs): | |
18 | """Resolve marshmallow Schemas in a dict mapping operation to OpenApi `Operation Object | |
19 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject`_""" | |
20 | ||
21 | for operation in operations.values(): | |
22 | if not isinstance(operation, dict): | |
23 | continue | |
24 | if "parameters" in operation: | |
25 | operation["parameters"] = self.resolve_parameters( | |
26 | operation["parameters"] | |
27 | ) | |
28 | if self.openapi_version.major >= 3: | |
29 | self.resolve_callback(operation.get("callbacks", {})) | |
30 | if "requestBody" in operation: | |
31 | self.resolve_schema(operation["requestBody"]) | |
32 | for response in operation.get("responses", {}).values(): | |
33 | self.resolve_response(response) | |
34 | ||
35 | def resolve_callback(self, callbacks): | |
36 | """Resolve marshmallow Schemas in a dict mapping callback name to OpenApi `Callback Object | |
37 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject`_. | |
38 | ||
39 | This is done recursively, so it is possible to define callbacks in your callbacks. | |
40 | ||
41 | Example: :: | |
42 | ||
43 | #Input | |
44 | { | |
45 | "userEvent": { | |
46 | "https://my.example/user-callback": { | |
47 | "post": { | |
48 | "requestBody": { | |
49 | "content": { | |
50 | "application/json": { | |
51 | "schema": UserSchema | |
52 | } | |
53 | } | |
54 | } | |
55 | }, | |
56 | } | |
57 | } | |
58 | } | |
59 | ||
60 | #Output | |
61 | { | |
62 | "userEvent": { | |
63 | "https://my.example/user-callback": { | |
64 | "post": { | |
65 | "requestBody": { | |
66 | "content": { | |
67 | "application/json": { | |
68 | "schema": { | |
69 | "$ref": "#/components/schemas/User" | |
70 | } | |
71 | } | |
72 | } | |
73 | } | |
74 | }, | |
75 | } | |
76 | } | |
77 | } | |
78 | ||
79 | ||
80 | """ | |
81 | for callback in callbacks.values(): | |
82 | if isinstance(callback, dict): | |
83 | for path in callback.values(): | |
84 | self.resolve_operations(path) | |
85 | ||
17 | 86 | def resolve_parameters(self, parameters): |
18 | 87 | """Resolve marshmallow Schemas in a list of OpenAPI `Parameter Objects |
19 | 88 | <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameter-object>`_. |
37 | 106 | |
38 | 107 | #Output |
39 | 108 | [ |
40 | {"in": "query", "name": "id", "required": False, "schema": {"type": "integer", "format": "int32"}}, | |
109 | {"in": "query", "name": "id", "required": False, "schema": {"type": "integer"}}, | |
41 | 110 | {"in": "query", "name": "name", "required": False, "schema": {"type": "string"}} |
42 | 111 | ] |
43 | 112 | |
75 | 144 | ): |
76 | 145 | schema_instance = resolve_schema_instance(parameter.pop("schema")) |
77 | 146 | resolved += self.converter.schema2parameters( |
78 | schema_instance, default_in=parameter.pop("in"), **parameter | |
147 | schema_instance, location=parameter.pop("in"), **parameter | |
79 | 148 | ) |
80 | 149 | else: |
81 | 150 | self.resolve_schema(parameter) |
161 | 230 | |
162 | 231 | def resolve_schema_dict(self, schema): |
163 | 232 | """Resolve a marshmallow Schema class, object, or a string that resolves |
164 | to a Schema class or an OpenAPI Schema Object containing one of the above | |
165 | to an OpenAPI Schema Object or Reference Object. | |
233 | to a Schema class or a schema reference or an OpenAPI Schema Object | |
234 | containing one of the above to an OpenAPI Schema Object or Reference Object. | |
166 | 235 | |
167 | 236 | If the input is a marshmallow Schema class, object or a string that resolves |
168 | 237 | to a Schema class the Schema will be translated to an OpenAPI Schema Object |
17 | 17 | |
18 | 18 | :param str name: Identifier by which schema may be referenced |
19 | 19 | :param dict definition: Schema definition |
20 | :param dict kwargs: All additional keywords arguments sent to `APISpec.schema()` | |
20 | :param kwargs: All additional keywords arguments sent to `APISpec.schema()` | |
21 | 21 | """ |
22 | 22 | raise PluginMethodNotImplementedError |
23 | 23 | |
25 | 25 | """May return response component description as a dict. |
26 | 26 | |
27 | 27 | :param dict response: Response fields |
28 | :param dict kwargs: All additional keywords arguments sent to `APISpec.response()` | |
28 | :param kwargs: All additional keywords arguments sent to `APISpec.response()` | |
29 | 29 | """ |
30 | 30 | raise PluginMethodNotImplementedError |
31 | 31 | |
33 | 33 | """May return parameter component description as a dict. |
34 | 34 | |
35 | 35 | :param dict parameter: Parameter fields |
36 | :param dict kwargs: All additional keywords arguments sent to `APISpec.parameter()` | |
36 | :param kwargs: All additional keywords arguments sent to `APISpec.parameter()` | |
37 | 37 | """ |
38 | 38 | raise PluginMethodNotImplementedError |
39 | 39 | |
46 | 46 | :param list parameters: A `list` of parameters objects or references for the path. See |
47 | 47 | https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject |
48 | 48 | and https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#referenceObject |
49 | :param dict kwargs: All additional keywords arguments sent to `APISpec.path()` | |
49 | :param kwargs: All additional keywords arguments sent to `APISpec.path()` | |
50 | 50 | |
51 | 51 | Return value should be a string or None. If a string is returned, it |
52 | 52 | is set as the path. |
63 | 63 | :param str path: Path to the resource |
64 | 64 | :param dict operations: A `dict` mapping HTTP methods to operation object. |
65 | 65 | See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operationObject |
66 | :param dict kwargs: All additional keywords arguments sent to `APISpec.path()` | |
66 | :param kwargs: All additional keywords arguments sent to `APISpec.path()` | |
67 | 67 | """ |
68 | 68 | raise PluginMethodNotImplementedError |
19 | 19 | "schema": "schemas", |
20 | 20 | "response": "responses", |
21 | 21 | "parameter": "parameters", |
22 | "header": "headers", | |
22 | 23 | "example": "examples", |
23 | 24 | "security_scheme": "securitySchemes", |
24 | 25 | }, |
102 | 103 | < self.MAX_EXCLUSIVE_VERSION |
103 | 104 | ): |
104 | 105 | raise exceptions.APISpecError( |
105 | "Not a valid OpenAPI version number: {}".format(openapi_version) | |
106 | f"Not a valid OpenAPI version number: {openapi_version}" | |
106 | 107 | ) |
107 | 108 | super().__init__(openapi_version) |
108 | 109 |
14 | 14 | yaml.add_representer(OrderedDict, YAMLDumper._represent_dict, Dumper=YAMLDumper) |
15 | 15 | |
16 | 16 | |
17 | def dict_to_yaml(dic): | |
18 | return yaml.dump(dic, Dumper=YAMLDumper) | |
17 | def dict_to_yaml(dic, yaml_dump_kwargs=None): | |
18 | if yaml_dump_kwargs is None: | |
19 | yaml_dump_kwargs = {} | |
20 | return yaml.dump(dic, Dumper=YAMLDumper, **yaml_dump_kwargs) | |
19 | 21 | |
20 | 22 | |
21 | 23 | def load_yaml_from_docstring(docstring): |
0 | 0 | from marshmallow import Schema, fields |
1 | ||
2 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
3 | 1 | |
4 | 2 | |
5 | 3 | class PetSchema(Schema): |
39 | 37 | |
40 | 38 | class SelfReferencingSchema(Schema): |
41 | 39 | id = fields.Int() |
42 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
43 | single = fields.Nested("self") | |
44 | many = fields.Nested("self", many=True) | |
45 | else: | |
46 | single = fields.Nested(lambda: SelfReferencingSchema()) | |
47 | many = fields.Nested(lambda: SelfReferencingSchema(many=True)) | |
40 | single = fields.Nested(lambda: SelfReferencingSchema()) | |
41 | many = fields.Nested(lambda: SelfReferencingSchema(many=True)) | |
48 | 42 | |
49 | 43 | |
50 | 44 | class OrderedSchema(Schema): |
16 | 16 | get_examples, |
17 | 17 | get_paths, |
18 | 18 | get_parameters, |
19 | get_headers, | |
19 | 20 | get_responses, |
20 | 21 | get_security_schemes, |
21 | 22 | build_ref, |
59 | 60 | version="1.0.0", |
60 | 61 | openapi_version=openapi_version, |
61 | 62 | info={"description": description}, |
62 | **security_kwargs | |
63 | **security_kwargs, | |
63 | 64 | ) |
64 | 65 | |
65 | 66 | |
201 | 202 | spec.components.response("test_response") |
202 | 203 | |
203 | 204 | def test_parameter(self, spec): |
205 | # Note: this is an OpenAPI v2 parameter header | |
206 | # but is does the job for the test even for OpenAPI v3 | |
204 | 207 | parameter = {"format": "int64", "type": "integer"} |
205 | 208 | spec.components.parameter("PetId", "path", parameter) |
206 | 209 | params = get_parameters(spec) |
225 | 228 | match='Another parameter with name "test_parameter" is already registered.', |
226 | 229 | ): |
227 | 230 | spec.components.parameter("test_parameter", "path") |
231 | ||
232 | # Referenced headers are only supported in OAS 3.x | |
233 | @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) | |
234 | def test_header(self, spec): | |
235 | header = {"schema": {"type": "string"}} | |
236 | spec.components.header("test_header", header.copy()) | |
237 | headers = get_headers(spec) | |
238 | assert headers["test_header"] == header | |
239 | ||
240 | # Referenced headers are only supported in OAS 3.x | |
241 | @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) | |
242 | def test_header_is_chainable(self, spec): | |
243 | header = {"schema": {"type": "string"}} | |
244 | spec.components.header("header1", header).header("header2", header) | |
245 | headers = get_headers(spec) | |
246 | assert "header1" in headers | |
247 | assert "header2" in headers | |
248 | ||
249 | # Referenced headers are only supported in OAS 3.x | |
250 | @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) | |
251 | def test_header_duplicate_name(self, spec): | |
252 | spec.components.header("test_header", {"schema": {"type": "string"}}) | |
253 | with pytest.raises( | |
254 | DuplicateComponentNameError, | |
255 | match='Another header with name "test_header" is already registered.', | |
256 | ): | |
257 | spec.components.header("test_header", {"schema": {"type": "integer"}}) | |
228 | 258 | |
229 | 259 | # Referenced examples are only supported in OAS 3.x |
230 | 260 | @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) |
306 | 336 | } |
307 | 337 | ], |
308 | 338 | "responses": { |
309 | "200": {"schema": "Pet", "description": "successful operation"}, | |
339 | "200": {"description": "successful operation"}, | |
310 | 340 | "400": {"description": "Invalid ID supplied"}, |
311 | 341 | "404": {"description": "Pet not found"}, |
312 | 342 | }, |
617 | 647 | with pytest.raises(APISpecError, match=message): |
618 | 648 | spec.path("/pet/{petId}", operations={"dummy": {}}) |
619 | 649 | |
650 | def test_path_resolve_response_schema(self, spec): | |
651 | schema = {"schema": "PetSchema"} | |
652 | if spec.openapi_version.major >= 3: | |
653 | schema = {"content": {"application/json": schema}} | |
654 | spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}}) | |
655 | resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] | |
656 | if spec.openapi_version.major < 3: | |
657 | schema = resp["schema"] | |
658 | else: | |
659 | schema = resp["content"]["application/json"]["schema"] | |
660 | assert schema == build_ref(spec, "schema", "PetSchema") | |
661 | ||
662 | # requestBody only exists in OAS 3 | |
663 | @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) | |
664 | def test_path_resolve_request_body(self, spec): | |
665 | spec.path( | |
666 | "/pet/{petId}", | |
667 | operations={ | |
668 | "get": { | |
669 | "requestBody": { | |
670 | "content": {"application/json": {"schema": "PetSchema"}} | |
671 | } | |
672 | } | |
673 | }, | |
674 | ) | |
675 | assert get_paths(spec)["/pet/{petId}"]["get"]["requestBody"]["content"][ | |
676 | "application/json" | |
677 | ]["schema"] == build_ref(spec, "schema", "PetSchema") | |
678 | ||
679 | # "headers" components section only exists in OAS 3 | |
680 | @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) | |
681 | def test_path_resolve_response_header(self, spec): | |
682 | response = {"headers": {"header_1": "Header_1"}} | |
683 | spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}}) | |
684 | resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] | |
685 | header_1 = resp["headers"]["header_1"] | |
686 | assert header_1 == build_ref(spec, "header", "Header_1") | |
687 | ||
688 | # "examples" components section only exists in OAS 3 | |
689 | @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) | |
690 | def test_path_resolve_response_examples(self, spec): | |
691 | response = { | |
692 | "content": {"application/json": {"examples": {"example_1": "Example_1"}}} | |
693 | } | |
694 | spec.path("/pet/{petId}", operations={"get": {"responses": {"200": response}}}) | |
695 | resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] | |
696 | example_1 = resp["content"]["application/json"]["examples"]["example_1"] | |
697 | assert example_1 == build_ref(spec, "example", "Example_1") | |
698 | ||
699 | # "examples" components section only exists in OAS 3 | |
700 | @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) | |
701 | def test_path_resolve_request_body_examples(self, spec): | |
702 | request_body = { | |
703 | "content": {"application/json": {"examples": {"example_1": "Example_1"}}} | |
704 | } | |
705 | spec.path("/pet/{petId}", operations={"get": {"requestBody": request_body}}) | |
706 | reqbdy = get_paths(spec)["/pet/{petId}"]["get"]["requestBody"] | |
707 | example_1 = reqbdy["content"]["application/json"]["examples"]["example_1"] | |
708 | assert example_1 == build_ref(spec, "example", "Example_1") | |
709 | ||
710 | # "examples" components section only exists in OAS 3 | |
711 | @pytest.mark.parametrize("spec", ("3.0.0",), indirect=True) | |
712 | def test_path_resolve_parameter_examples(self, spec): | |
713 | parameter = { | |
714 | "name": "test", | |
715 | "in": "query", | |
716 | "examples": {"example_1": "Example_1"}, | |
717 | } | |
718 | spec.path("/pet/{petId}", operations={"get": {"parameters": [parameter]}}) | |
719 | param = get_paths(spec)["/pet/{petId}"]["get"]["parameters"][0] | |
720 | example_1 = param["examples"]["example_1"] | |
721 | assert example_1 == build_ref(spec, "example", "Example_1") | |
722 | ||
620 | 723 | |
621 | 724 | class TestPlugins: |
622 | 725 | @staticmethod |
741 | 844 | self.output = output |
742 | 845 | |
743 | 846 | def path_helper(self, path, operations, **kwargs): |
744 | self.output.append("plugin_{}_path".format(self.index)) | |
847 | self.output.append(f"plugin_{self.index}_path") | |
745 | 848 | |
746 | 849 | def operation_helper(self, path, operations, **kwargs): |
747 | self.output.append("plugin_{}_operations".format(self.index)) | |
850 | self.output.append(f"plugin_{self.index}_operations") | |
748 | 851 | |
749 | 852 | def test_plugins_order(self): |
750 | 853 | """Test plugins execution order in APISpec.path |
6 | 6 | |
7 | 7 | from apispec import APISpec |
8 | 8 | from apispec.ext.marshmallow import MarshmallowPlugin |
9 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
10 | 9 | from apispec.ext.marshmallow import common |
11 | 10 | from apispec.exceptions import APISpecError |
12 | 11 | from .schemas import ( |
232 | 231 | reference = parameter["content"]["application/json"]["schema"] |
233 | 232 | assert reference == build_ref(spec, "schema", "Pet") |
234 | 233 | |
235 | resolved_schema = spec.components._schemas["Pet"] | |
234 | resolved_schema = spec.components.schemas["Pet"] | |
236 | 235 | assert resolved_schema["properties"]["name"]["type"] == "string" |
237 | 236 | assert resolved_schema["properties"]["password"]["type"] == "string" |
238 | 237 | assert resolved_schema["properties"]["id"]["type"] == "integer" |
253 | 252 | reference = response["content"]["application/json"]["schema"] |
254 | 253 | assert reference == build_ref(spec, "schema", "Pet") |
255 | 254 | |
256 | resolved_schema = spec.components._schemas["Pet"] | |
255 | resolved_schema = spec.components.schemas["Pet"] | |
257 | 256 | assert resolved_schema["properties"]["id"]["type"] == "integer" |
258 | 257 | assert resolved_schema["properties"]["name"]["type"] == "string" |
259 | 258 | assert resolved_schema["properties"]["password"]["type"] == "string" |
266 | 265 | reference = response["headers"]["PetHeader"]["schema"] |
267 | 266 | assert reference == build_ref(spec, "schema", "Pet") |
268 | 267 | |
269 | resolved_schema = spec.components._schemas["Pet"] | |
268 | resolved_schema = spec.components.schemas["Pet"] | |
270 | 269 | assert resolved_schema["properties"]["id"]["type"] == "integer" |
271 | 270 | assert resolved_schema["properties"]["name"]["type"] == "string" |
272 | 271 | assert resolved_schema["properties"]["password"]["type"] == "string" |
327 | 326 | |
328 | 327 | |
329 | 328 | class TestOperationHelper: |
329 | @pytest.fixture | |
330 | def make_pet_callback_spec(self, spec_fixture): | |
331 | def _make_pet_spec(operations): | |
332 | spec_fixture.spec.path( | |
333 | path="/pet", | |
334 | operations={ | |
335 | "post": {"callbacks": {"petEvent": {"petCallbackUrl": operations}}} | |
336 | }, | |
337 | ) | |
338 | return spec_fixture | |
339 | ||
340 | return _make_pet_spec | |
341 | ||
330 | 342 | @pytest.mark.parametrize( |
331 | 343 | "pet_schema", |
332 | 344 | (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"), |
363 | 375 | header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"] |
364 | 376 | assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet") |
365 | 377 | assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet") |
366 | assert len(spec_fixture.spec.components._schemas) == 1 | |
367 | resolved_schema = spec_fixture.spec.components._schemas["Pet"] | |
378 | assert len(spec_fixture.spec.components.schemas) == 1 | |
379 | resolved_schema = spec_fixture.spec.components.schemas["Pet"] | |
368 | 380 | assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema) |
369 | 381 | assert get["responses"]["200"]["description"] == "successful operation" |
370 | 382 | |
412 | 424 | |
413 | 425 | assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet") |
414 | 426 | assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet") |
415 | assert len(spec_fixture.spec.components._schemas) == 1 | |
416 | resolved_schema = spec_fixture.spec.components._schemas["Pet"] | |
427 | assert len(spec_fixture.spec.components.schemas) == 1 | |
428 | resolved_schema = spec_fixture.spec.components.schemas["Pet"] | |
429 | assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema) | |
430 | assert get["responses"]["200"]["description"] == "successful operation" | |
431 | ||
432 | @pytest.mark.parametrize( | |
433 | "pet_schema", | |
434 | (PetSchema, PetSchema(), PetSchema(many=True), "tests.schemas.PetSchema"), | |
435 | ) | |
436 | @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) | |
437 | def test_callback_schema_v3(self, make_pet_callback_spec, pet_schema): | |
438 | spec_fixture = make_pet_callback_spec( | |
439 | { | |
440 | "get": { | |
441 | "responses": { | |
442 | "200": { | |
443 | "content": {"application/json": {"schema": pet_schema}}, | |
444 | "description": "successful operation", | |
445 | "headers": {"PetHeader": {"schema": pet_schema}}, | |
446 | } | |
447 | } | |
448 | } | |
449 | } | |
450 | ) | |
451 | p = get_paths(spec_fixture.spec)["/pet"] | |
452 | c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] | |
453 | get = c["get"] | |
454 | if isinstance(pet_schema, Schema) and pet_schema.many is True: | |
455 | assert ( | |
456 | get["responses"]["200"]["content"]["application/json"]["schema"]["type"] | |
457 | == "array" | |
458 | ) | |
459 | schema_reference = get["responses"]["200"]["content"]["application/json"][ | |
460 | "schema" | |
461 | ]["items"] | |
462 | assert ( | |
463 | get["responses"]["200"]["headers"]["PetHeader"]["schema"]["type"] | |
464 | == "array" | |
465 | ) | |
466 | header_reference = get["responses"]["200"]["headers"]["PetHeader"][ | |
467 | "schema" | |
468 | ]["items"] | |
469 | else: | |
470 | schema_reference = get["responses"]["200"]["content"]["application/json"][ | |
471 | "schema" | |
472 | ] | |
473 | header_reference = get["responses"]["200"]["headers"]["PetHeader"]["schema"] | |
474 | ||
475 | assert schema_reference == build_ref(spec_fixture.spec, "schema", "Pet") | |
476 | assert header_reference == build_ref(spec_fixture.spec, "schema", "Pet") | |
477 | assert len(spec_fixture.spec.components.schemas) == 1 | |
478 | resolved_schema = spec_fixture.spec.components.schemas["Pet"] | |
417 | 479 | assert resolved_schema == spec_fixture.openapi.schema2jsonschema(PetSchema) |
418 | 480 | assert get["responses"]["200"]["description"] == "successful operation" |
419 | 481 | |
439 | 501 | p = get_paths(spec_fixture.spec)["/pet"] |
440 | 502 | get = p["get"] |
441 | 503 | assert get["parameters"] == spec_fixture.openapi.schema2parameters( |
442 | PetSchema(), default_in="query" | |
504 | PetSchema(), location="query" | |
443 | 505 | ) |
444 | 506 | post = p["post"] |
445 | 507 | assert post["parameters"] == spec_fixture.openapi.schema2parameters( |
446 | 508 | PetSchema, |
447 | default_in="body", | |
509 | location="body", | |
448 | 510 | required=True, |
449 | 511 | name="pet", |
450 | 512 | description="a pet schema", |
468 | 530 | p = get_paths(spec_fixture.spec)["/pet"] |
469 | 531 | get = p["get"] |
470 | 532 | assert get["parameters"] == spec_fixture.openapi.schema2parameters( |
471 | PetSchema(), default_in="query" | |
533 | PetSchema(), location="query" | |
472 | 534 | ) |
473 | 535 | for parameter in get["parameters"]: |
474 | 536 | description = parameter.get("description", False) |
485 | 547 | assert post["requestBody"]["description"] == "a pet schema" |
486 | 548 | assert post["requestBody"]["required"] |
487 | 549 | |
550 | @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) | |
551 | def test_callback_schema_expand_parameters_v3(self, make_pet_callback_spec): | |
552 | spec_fixture = make_pet_callback_spec( | |
553 | { | |
554 | "get": {"parameters": [{"in": "query", "schema": PetSchema}]}, | |
555 | "post": { | |
556 | "requestBody": { | |
557 | "description": "a pet schema", | |
558 | "required": True, | |
559 | "content": {"application/json": {"schema": PetSchema}}, | |
560 | } | |
561 | }, | |
562 | } | |
563 | ) | |
564 | p = get_paths(spec_fixture.spec)["/pet"] | |
565 | c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] | |
566 | get = c["get"] | |
567 | assert get["parameters"] == spec_fixture.openapi.schema2parameters( | |
568 | PetSchema(), location="query" | |
569 | ) | |
570 | for parameter in get["parameters"]: | |
571 | description = parameter.get("description", False) | |
572 | assert description | |
573 | name = parameter["name"] | |
574 | assert description == PetSchema.description[name] | |
575 | post = c["post"] | |
576 | post_schema = spec_fixture.marshmallow_plugin.resolver.resolve_schema_dict( | |
577 | PetSchema | |
578 | ) | |
579 | assert ( | |
580 | post["requestBody"]["content"]["application/json"]["schema"] == post_schema | |
581 | ) | |
582 | assert post["requestBody"]["description"] == "a pet schema" | |
583 | assert post["requestBody"]["required"] | |
584 | ||
488 | 585 | @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) |
489 | 586 | def test_schema_uses_ref_if_available_v2(self, spec_fixture): |
490 | 587 | spec_fixture.spec.components.schema("Pet", schema=PetSchema) |
510 | 607 | }, |
511 | 608 | ) |
512 | 609 | get = get_paths(spec_fixture.spec)["/pet"]["get"] |
610 | assert get["responses"]["200"]["content"]["application/json"][ | |
611 | "schema" | |
612 | ] == build_ref(spec_fixture.spec, "schema", "Pet") | |
613 | ||
614 | @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) | |
615 | def test_callback_schema_uses_ref_if_available_v3(self, make_pet_callback_spec): | |
616 | spec_fixture = make_pet_callback_spec( | |
617 | { | |
618 | "get": { | |
619 | "responses": { | |
620 | "200": {"content": {"application/json": {"schema": PetSchema}}} | |
621 | } | |
622 | } | |
623 | } | |
624 | ) | |
625 | p = get_paths(spec_fixture.spec)["/pet"] | |
626 | c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] | |
627 | get = c["get"] | |
513 | 628 | assert get["responses"]["200"]["content"]["application/json"][ |
514 | 629 | "schema" |
515 | 630 | ] == build_ref(spec_fixture.spec, "schema", "Pet") |
606 | 721 | in get["responses"]["200"]["content"]["application/json"]["schema"] |
607 | 722 | ) |
608 | 723 | |
724 | def test_callback_schema_uses_ref_if_available_name_resolver_returns_none_v3(self): | |
725 | def resolver(schema): | |
726 | return None | |
727 | ||
728 | spec = APISpec( | |
729 | title="Test auto-reference", | |
730 | version="0.1", | |
731 | openapi_version="3.0.0", | |
732 | plugins=(MarshmallowPlugin(schema_name_resolver=resolver),), | |
733 | ) | |
734 | spec.components.schema("Pet", schema=PetSchema) | |
735 | spec.path( | |
736 | path="/pet", | |
737 | operations={ | |
738 | "post": { | |
739 | "callbacks": { | |
740 | "petEvent": { | |
741 | "petCallbackUrl": { | |
742 | "get": { | |
743 | "responses": { | |
744 | "200": { | |
745 | "content": { | |
746 | "application/json": { | |
747 | "schema": PetSchema | |
748 | } | |
749 | } | |
750 | } | |
751 | } | |
752 | } | |
753 | } | |
754 | } | |
755 | } | |
756 | } | |
757 | }, | |
758 | ) | |
759 | p = get_paths(spec)["/pet"] | |
760 | c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] | |
761 | get = c["get"] | |
762 | assert get["responses"]["200"]["content"]["application/json"][ | |
763 | "schema" | |
764 | ] == build_ref(spec, "schema", "Pet") | |
765 | ||
609 | 766 | @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) |
610 | 767 | def test_schema_uses_ref_in_parameters_and_request_body_if_available_v2( |
611 | 768 | self, spec_fixture |
645 | 802 | p = get_paths(spec_fixture.spec)["/pet"] |
646 | 803 | assert "schema" in p["get"]["parameters"][0] |
647 | 804 | post = p["post"] |
805 | schema_ref = post["requestBody"]["content"]["application/json"]["schema"] | |
806 | assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet") | |
807 | ||
808 | @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) | |
809 | def test_callback_schema_uses_ref_in_parameters_and_request_body_if_available_v3( | |
810 | self, make_pet_callback_spec | |
811 | ): | |
812 | spec_fixture = make_pet_callback_spec( | |
813 | { | |
814 | "get": {"parameters": [{"in": "query", "schema": PetSchema}]}, | |
815 | "post": { | |
816 | "requestBody": { | |
817 | "content": {"application/json": {"schema": PetSchema}} | |
818 | } | |
819 | }, | |
820 | } | |
821 | ) | |
822 | p = get_paths(spec_fixture.spec)["/pet"] | |
823 | c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] | |
824 | assert "schema" in c["get"]["parameters"][0] | |
825 | post = c["post"] | |
648 | 826 | schema_ref = post["requestBody"]["content"]["application/json"]["schema"] |
649 | 827 | assert schema_ref == build_ref(spec_fixture.spec, "schema", "Pet") |
650 | 828 | |
720 | 898 | ] |
721 | 899 | assert response_schema == resolved_schema |
722 | 900 | |
901 | @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) | |
902 | def test_callback_schema_array_uses_ref_if_available_v3( | |
903 | self, make_pet_callback_spec | |
904 | ): | |
905 | spec_fixture = make_pet_callback_spec( | |
906 | { | |
907 | "get": { | |
908 | "parameters": [ | |
909 | { | |
910 | "name": "Pet", | |
911 | "in": "query", | |
912 | "content": { | |
913 | "application/json": { | |
914 | "schema": {"type": "array", "items": PetSchema} | |
915 | } | |
916 | }, | |
917 | } | |
918 | ], | |
919 | "responses": { | |
920 | "200": { | |
921 | "content": { | |
922 | "application/json": { | |
923 | "schema": {"type": "array", "items": PetSchema} | |
924 | } | |
925 | } | |
926 | } | |
927 | }, | |
928 | } | |
929 | } | |
930 | ) | |
931 | p = get_paths(spec_fixture.spec)["/pet"] | |
932 | c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] | |
933 | get = c["get"] | |
934 | assert len(get["parameters"]) == 1 | |
935 | resolved_schema = { | |
936 | "type": "array", | |
937 | "items": build_ref(spec_fixture.spec, "schema", "Pet"), | |
938 | } | |
939 | request_schema = get["parameters"][0]["content"]["application/json"]["schema"] | |
940 | assert request_schema == resolved_schema | |
941 | response_schema = get["responses"]["200"]["content"]["application/json"][ | |
942 | "schema" | |
943 | ] | |
944 | assert response_schema == resolved_schema | |
945 | ||
723 | 946 | @pytest.mark.parametrize("spec_fixture", ("2.0",), indirect=True) |
724 | 947 | def test_schema_partially_v2(self, spec_fixture): |
725 | 948 | spec_fixture.spec.components.schema("Pet", schema=PetSchema) |
784 | 1007 | }, |
785 | 1008 | } |
786 | 1009 | |
1010 | @pytest.mark.parametrize("spec_fixture", ("3.0.0",), indirect=True) | |
1011 | def test_callback_schema_partially_v3(self, make_pet_callback_spec): | |
1012 | spec_fixture = make_pet_callback_spec( | |
1013 | { | |
1014 | "get": { | |
1015 | "responses": { | |
1016 | "200": { | |
1017 | "content": { | |
1018 | "application/json": { | |
1019 | "schema": { | |
1020 | "type": "object", | |
1021 | "properties": { | |
1022 | "mother": PetSchema, | |
1023 | "father": PetSchema, | |
1024 | }, | |
1025 | } | |
1026 | } | |
1027 | } | |
1028 | } | |
1029 | } | |
1030 | } | |
1031 | } | |
1032 | ) | |
1033 | p = get_paths(spec_fixture.spec)["/pet"] | |
1034 | c = p["post"]["callbacks"]["petEvent"]["petCallbackUrl"] | |
1035 | get = c["get"] | |
1036 | assert get["responses"]["200"]["content"]["application/json"]["schema"] == { | |
1037 | "type": "object", | |
1038 | "properties": { | |
1039 | "mother": build_ref(spec_fixture.spec, "schema", "Pet"), | |
1040 | "father": build_ref(spec_fixture.spec, "schema", "Pet"), | |
1041 | }, | |
1042 | } | |
1043 | ||
787 | 1044 | def test_parameter_reference(self, spec_fixture): |
788 | 1045 | if spec_fixture.spec.openapi_version.major < 3: |
789 | 1046 | param = {"schema": PetSchema} |
818 | 1075 | |
819 | 1076 | def test_schema_global_state_untouched_2parameters(self, spec_fixture): |
820 | 1077 | assert get_nested_schema(RunSchema, "sample") is None |
821 | data = spec_fixture.openapi.schema2parameters(RunSchema) | |
1078 | data = spec_fixture.openapi.schema2parameters(RunSchema, location="json") | |
822 | 1079 | json.dumps(data) |
823 | 1080 | assert get_nested_schema(RunSchema, "sample") is None |
1081 | ||
1082 | def test_resolve_schema_dict_ref_as_string(self, spec): | |
1083 | schema = {"schema": "PetSchema"} | |
1084 | if spec.openapi_version.major >= 3: | |
1085 | schema = {"content": {"application/json": schema}} | |
1086 | spec.path("/pet/{petId}", operations={"get": {"responses": {"200": schema}}}) | |
1087 | resp = get_paths(spec)["/pet/{petId}"]["get"]["responses"]["200"] | |
1088 | if spec.openapi_version.major < 3: | |
1089 | schema = resp["schema"] | |
1090 | else: | |
1091 | schema = resp["content"]["application/json"]["schema"] | |
1092 | assert schema == build_ref(spec, "schema", "PetSchema") | |
824 | 1093 | |
825 | 1094 | |
826 | 1095 | class TestCircularReference: |
882 | 1151 | assert "default" not in props["numbers"] |
883 | 1152 | |
884 | 1153 | |
885 | @pytest.mark.skipif( | |
886 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="Values ignored in marshmallow 2" | |
887 | ) | |
888 | 1154 | class TestDictValues: |
889 | 1155 | def test_dict_values_resolve_to_additional_properties(self, spec): |
890 | 1156 | class SchemaWithDict(Schema): |
2 | 2 | |
3 | 3 | import pytest |
4 | 4 | from marshmallow import fields, validate |
5 | ||
6 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
7 | 5 | |
8 | 6 | from .schemas import CategorySchema, CustomList, CustomStringField, CustomIntegerField |
9 | 7 | from .utils import build_ref |
60 | 58 | @pytest.mark.parametrize( |
61 | 59 | ("FieldClass", "expected_format"), |
62 | 60 | [ |
63 | (fields.Integer, "int32"), | |
64 | (fields.Float, "float"), | |
65 | 61 | (fields.UUID, "uuid"), |
66 | 62 | (fields.DateTime, "date-time"), |
67 | 63 | (fields.Date, "date"), |
94 | 90 | |
95 | 91 | |
96 | 92 | def test_datetime_field_with_missing(spec_fixture): |
97 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
98 | field = fields.Date(missing=dt.date(2014, 7, 18).isoformat()) | |
99 | else: | |
100 | field = fields.Date(missing=dt.date(2014, 7, 18)) | |
93 | field = fields.Date(missing=dt.date(2014, 7, 18)) | |
101 | 94 | res = spec_fixture.openapi.field2property(field) |
102 | 95 | assert res["default"] == dt.date(2014, 7, 18).isoformat() |
103 | 96 | |
292 | 285 | assert properties["x-customString"] == ( |
293 | 286 | spec_fixture.openapi.openapi_version == "2.0" |
294 | 287 | ) |
288 | ||
289 | ||
290 | def test_field2property_with_non_string_metadata_keys(spec_fixture): | |
291 | class _DesertSentinel: | |
292 | pass | |
293 | ||
294 | field = fields.Boolean(description="A description") | |
295 | field.metadata[_DesertSentinel()] = "to be ignored" | |
296 | result = spec_fixture.openapi.field2property(field) | |
297 | assert result == {"description": "A description", "type": "boolean"} |
0 | 0 | import pytest |
1 | ||
2 | from marshmallow import fields, Schema, validate | |
1 | from datetime import datetime | |
2 | ||
3 | from marshmallow import fields, INCLUDE, RAISE, Schema, validate | |
3 | 4 | |
4 | 5 | from apispec.ext.marshmallow import MarshmallowPlugin |
5 | from apispec.ext.marshmallow.openapi import MARSHMALLOW_VERSION_INFO | |
6 | 6 | from apispec import exceptions, utils, APISpec |
7 | 7 | |
8 | 8 | from .schemas import CustomList, CustomStringField |
11 | 11 | |
12 | 12 | class TestMarshmallowFieldToOpenAPI: |
13 | 13 | def test_fields_with_missing_load(self, openapi): |
14 | field_dict = {"field": fields.Str(default="foo", missing="bar")} | |
15 | res = openapi.fields2parameters(field_dict, default_in="query") | |
14 | class MySchema(Schema): | |
15 | field = fields.Str(default="foo", missing="bar") | |
16 | ||
17 | res = openapi.schema2parameters(MySchema, location="query") | |
16 | 18 | if openapi.openapi_version.major < 3: |
17 | 19 | assert res[0]["default"] == "bar" |
18 | 20 | else: |
19 | 21 | assert res[0]["schema"]["default"] == "bar" |
20 | ||
21 | def test_fields_with_location(self, openapi): | |
22 | field_dict = {"field": fields.Str(location="querystring")} | |
23 | res = openapi.fields2parameters(field_dict, default_in="headers") | |
24 | assert res[0]["in"] == "query" | |
25 | ||
26 | # json/body is invalid for OpenAPI 3 | |
27 | @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) | |
28 | def test_fields_with_multiple_json_locations(self, openapi): | |
29 | field_dict = { | |
30 | "field1": fields.Str(location="json", required=True), | |
31 | "field2": fields.Str(location="json", required=True), | |
32 | "field3": fields.Str(location="json"), | |
33 | } | |
34 | res = openapi.fields2parameters(field_dict, default_in=None) | |
35 | assert len(res) == 1 | |
36 | assert res[0]["in"] == "body" | |
37 | assert res[0]["required"] is False | |
38 | assert "field1" in res[0]["schema"]["properties"] | |
39 | assert "field2" in res[0]["schema"]["properties"] | |
40 | assert "field3" in res[0]["schema"]["properties"] | |
41 | assert "required" in res[0]["schema"] | |
42 | assert len(res[0]["schema"]["required"]) == 2 | |
43 | assert "field1" in res[0]["schema"]["required"] | |
44 | assert "field2" in res[0]["schema"]["required"] | |
45 | ||
46 | def test_fields2parameters_does_not_modify_metadata(self, openapi): | |
47 | field_dict = {"field": fields.Str(location="querystring")} | |
48 | res = openapi.fields2parameters(field_dict, default_in="headers") | |
49 | assert res[0]["in"] == "query" | |
50 | ||
51 | res = openapi.fields2parameters(field_dict, default_in="headers") | |
52 | assert res[0]["in"] == "query" | |
53 | ||
54 | def test_fields_location_mapping(self, openapi): | |
55 | field_dict = {"field": fields.Str(location="cookies")} | |
56 | res = openapi.fields2parameters(field_dict, default_in="headers") | |
57 | assert res[0]["in"] == "cookie" | |
58 | ||
59 | def test_fields_default_location_mapping(self, openapi): | |
60 | field_dict = {"field": fields.Str()} | |
61 | res = openapi.fields2parameters(field_dict, default_in="headers") | |
62 | assert res[0]["in"] == "header" | |
63 | 22 | |
64 | 23 | # json/body is invalid for OpenAPI 3 |
65 | 24 | @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) |
68 | 27 | id = fields.Int() |
69 | 28 | |
70 | 29 | schema = ExampleSchema(many=True) |
71 | res = openapi.schema2parameters(schema=schema, default_in="json") | |
30 | res = openapi.schema2parameters(schema=schema, location="json") | |
72 | 31 | assert res[0]["in"] == "body" |
73 | 32 | |
74 | 33 | def test_fields_with_dump_only(self, openapi): |
75 | 34 | class UserSchema(Schema): |
76 | 35 | name = fields.Str(dump_only=True) |
77 | 36 | |
78 | res = openapi.fields2parameters(UserSchema._declared_fields, default_in="query") | |
37 | res = openapi.schema2parameters(schema=UserSchema(), location="query") | |
79 | 38 | assert len(res) == 0 |
80 | res = openapi.fields2parameters(UserSchema().fields, default_in="query") | |
39 | ||
40 | class UserSchema(Schema): | |
41 | name = fields.Str() | |
42 | ||
43 | class Meta: | |
44 | dump_only = ("name",) | |
45 | ||
46 | res = openapi.schema2parameters(schema=UserSchema(), location="query") | |
81 | 47 | assert len(res) == 0 |
82 | 48 | |
83 | class UserSchema(Schema): | |
84 | name = fields.Str() | |
85 | ||
86 | class Meta: | |
87 | dump_only = ("name",) | |
88 | ||
89 | res = openapi.schema2parameters(schema=UserSchema, default_in="query") | |
90 | assert len(res) == 0 | |
91 | ||
92 | 49 | |
93 | 50 | class TestMarshmallowSchemaToModelDefinition: |
94 | def test_invalid_schema(self, openapi): | |
95 | with pytest.raises(ValueError): | |
96 | openapi.schema2jsonschema(None) | |
97 | ||
98 | 51 | def test_schema2jsonschema_with_explicit_fields(self, openapi): |
99 | 52 | class UserSchema(Schema): |
100 | 53 | _id = fields.Int() |
113 | 66 | assert props["email"]["format"] == "email" |
114 | 67 | assert props["email"]["description"] == "email address of the user" |
115 | 68 | |
116 | @pytest.mark.skipif( | |
117 | MARSHMALLOW_VERSION_INFO[0] >= 3, reason="Behaviour changed in marshmallow 3" | |
118 | ) | |
119 | def test_schema2jsonschema_override_name_ma2(self, openapi): | |
120 | class ExampleSchema(Schema): | |
121 | _id = fields.Int(load_from="id", dump_to="id") | |
122 | _dt = fields.Int(load_from="lf_no_match", dump_to="dt") | |
123 | _lf = fields.Int(load_from="lf") | |
124 | _global = fields.Int(load_from="global", dump_to="global") | |
125 | ||
126 | class Meta: | |
127 | exclude = ("_global",) | |
128 | ||
129 | res = openapi.schema2jsonschema(ExampleSchema) | |
130 | assert res["type"] == "object" | |
131 | props = res["properties"] | |
132 | # `_id` renamed to `id` | |
133 | assert "_id" not in props and props["id"]["type"] == "integer" | |
134 | # `load_from` and `dump_to` do not match, `dump_to` is used | |
135 | assert "lf_no_match" not in props | |
136 | assert props["dt"]["type"] == "integer" | |
137 | # `load_from` and no `dump_to`, `load_from` is used | |
138 | assert props["lf"]["type"] == "integer" | |
139 | # `_global` excluded correctly | |
140 | assert "_global" not in props and "global" not in props | |
141 | ||
142 | @pytest.mark.skipif( | |
143 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="Behaviour changed in marshmallow 3" | |
144 | ) | |
145 | def test_schema2jsonschema_override_name_ma3(self, openapi): | |
69 | def test_schema2jsonschema_override_name(self, openapi): | |
146 | 70 | class ExampleSchema(Schema): |
147 | 71 | _id = fields.Int(data_key="id") |
148 | 72 | _global = fields.Int(data_key="global") |
206 | 130 | |
207 | 131 | res = openapi.schema2jsonschema(WhiteStripesSchema) |
208 | 132 | assert set(res["properties"].keys()) == {"guitarist", "drummer"} |
133 | ||
134 | def test_unknown_values_disallow(self, openapi): | |
135 | class UnknownRaiseSchema(Schema): | |
136 | class Meta: | |
137 | unknown = RAISE | |
138 | ||
139 | first = fields.Str() | |
140 | ||
141 | res = openapi.schema2jsonschema(UnknownRaiseSchema) | |
142 | assert res["additionalProperties"] is False | |
143 | ||
144 | def test_unknown_values_allow(self, openapi): | |
145 | class UnknownIncludeSchema(Schema): | |
146 | class Meta: | |
147 | unknown = INCLUDE | |
148 | ||
149 | first = fields.Str() | |
150 | ||
151 | res = openapi.schema2jsonschema(UnknownIncludeSchema) | |
152 | assert res["additionalProperties"] is True | |
209 | 153 | |
210 | 154 | def test_only_explicitly_declared_fields_are_translated(self, openapi): |
211 | 155 | class UserSchema(Schema): |
226 | 170 | assert "email" not in props |
227 | 171 | |
228 | 172 | def test_observed_field_name_for_required_field(self, openapi): |
229 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
230 | fields_dict = { | |
231 | "user_id": fields.Int(load_from="id", dump_to="id", required=True) | |
232 | } | |
233 | else: | |
234 | fields_dict = {"user_id": fields.Int(data_key="id", required=True)} | |
235 | ||
173 | fields_dict = {"user_id": fields.Int(data_key="id", required=True)} | |
236 | 174 | res = openapi.fields2jsonschema(fields_dict) |
237 | 175 | assert res["required"] == ["id"] |
238 | 176 | |
260 | 198 | class TestMarshmallowSchemaToParameters: |
261 | 199 | @pytest.mark.parametrize("ListClass", [fields.List, CustomList]) |
262 | 200 | def test_field_multiple(self, ListClass, openapi): |
263 | field = ListClass(fields.Str, location="querystring") | |
264 | res = openapi.field2parameter(field, name="field", default_in=None) | |
201 | field = ListClass(fields.Str) | |
202 | res = openapi._field2parameter(field, name="field", location="query") | |
265 | 203 | assert res["in"] == "query" |
266 | 204 | if openapi.openapi_version.major < 3: |
267 | 205 | assert res["type"] == "array" |
274 | 212 | assert res["explode"] is True |
275 | 213 | |
276 | 214 | def test_field_required(self, openapi): |
277 | field = fields.Str(required=True, location="query") | |
278 | res = openapi.field2parameter(field, name="field", default_in=None) | |
215 | field = fields.Str(required=True) | |
216 | res = openapi._field2parameter(field, name="field", location="query") | |
279 | 217 | assert res["required"] is True |
280 | ||
281 | def test_invalid_schema(self, openapi): | |
282 | with pytest.raises(ValueError): | |
283 | openapi.schema2parameters(None) | |
284 | 218 | |
285 | 219 | # json/body is invalid for OpenAPI 3 |
286 | 220 | @pytest.mark.parametrize("openapi", ("2.0",), indirect=True) |
289 | 223 | name = fields.Str() |
290 | 224 | email = fields.Email() |
291 | 225 | |
292 | res = openapi.schema2parameters(UserSchema, default_in="body") | |
226 | res = openapi.schema2parameters(UserSchema, location="body") | |
293 | 227 | assert len(res) == 1 |
294 | 228 | param = res[0] |
295 | 229 | assert param["in"] == "body" |
302 | 236 | name = fields.Str() |
303 | 237 | email = fields.Email(dump_only=True) |
304 | 238 | |
305 | res_nodump = openapi.schema2parameters(UserSchema, default_in="body") | |
239 | res_nodump = openapi.schema2parameters(UserSchema, location="body") | |
306 | 240 | assert len(res_nodump) == 1 |
307 | 241 | param = res_nodump[0] |
308 | 242 | assert param["in"] == "body" |
315 | 249 | name = fields.Str() |
316 | 250 | email = fields.Email() |
317 | 251 | |
318 | res = openapi.schema2parameters(UserSchema(many=True), default_in="body") | |
252 | res = openapi.schema2parameters(UserSchema(many=True), location="body") | |
319 | 253 | assert len(res) == 1 |
320 | 254 | param = res[0] |
321 | 255 | assert param["in"] == "body" |
327 | 261 | name = fields.Str() |
328 | 262 | email = fields.Email() |
329 | 263 | |
330 | res = openapi.schema2parameters(UserSchema, default_in="query") | |
264 | res = openapi.schema2parameters(UserSchema, location="query") | |
331 | 265 | assert len(res) == 2 |
332 | 266 | res.sort(key=lambda param: param["name"]) |
333 | 267 | assert res[0]["name"] == "email" |
340 | 274 | name = fields.Str() |
341 | 275 | email = fields.Email() |
342 | 276 | |
343 | res = openapi.schema2parameters(UserSchema(), default_in="query") | |
277 | res = openapi.schema2parameters(UserSchema(), location="query") | |
344 | 278 | assert len(res) == 2 |
345 | 279 | res.sort(key=lambda param: param["name"]) |
346 | 280 | assert res[0]["name"] == "email" |
354 | 288 | email = fields.Email() |
355 | 289 | |
356 | 290 | with pytest.raises(AssertionError): |
357 | openapi.schema2parameters(UserSchema(many=True), default_in="query") | |
291 | openapi.schema2parameters(UserSchema(many=True), location="query") | |
358 | 292 | |
359 | 293 | def test_fields_query(self, openapi): |
360 | field_dict = {"name": fields.Str(), "email": fields.Email()} | |
361 | res = openapi.fields2parameters(field_dict, default_in="query") | |
294 | class MySchema(Schema): | |
295 | name = fields.Str() | |
296 | email = fields.Email() | |
297 | ||
298 | res = openapi.schema2parameters(MySchema, location="query") | |
362 | 299 | assert len(res) == 2 |
363 | 300 | res.sort(key=lambda param: param["name"]) |
364 | 301 | assert res[0]["name"] == "email" |
476 | 413 | "required": True, |
477 | 414 | "type": "string", |
478 | 415 | }, |
479 | openapi.field2parameter( | |
416 | openapi._field2parameter( | |
480 | 417 | field=fields.List( |
481 | fields.Str(), | |
482 | validate=validate.OneOf(["freddie", "roger"]), | |
483 | location="querystring", | |
418 | fields.Str(), validate=validate.OneOf(["freddie", "roger"]), | |
484 | 419 | ), |
485 | default_in=None, | |
420 | location="query", | |
486 | 421 | name="body", |
487 | 422 | ), |
488 | 423 | ] |
489 | + openapi.schema2parameters(PageSchema, default_in="query"), | |
424 | + openapi.schema2parameters(PageSchema, location="query"), | |
490 | 425 | "responses": {200: {"schema": PetSchema, "description": "A pet"}}, |
491 | 426 | }, |
492 | 427 | "post": { |
499 | 434 | "type": "string", |
500 | 435 | } |
501 | 436 | ] |
502 | + openapi.schema2parameters(CategorySchema, default_in="body") | |
437 | + openapi.schema2parameters(CategorySchema, location="body") | |
503 | 438 | ), |
504 | 439 | "responses": {201: {"schema": PetSchema, "description": "A pet"}}, |
505 | 440 | }, |
534 | 469 | "required": True, |
535 | 470 | "schema": {"type": "string"}, |
536 | 471 | }, |
537 | openapi.field2parameter( | |
472 | openapi._field2parameter( | |
538 | 473 | field=fields.List( |
539 | fields.Str(), | |
540 | validate=validate.OneOf(["freddie", "roger"]), | |
541 | location="querystring", | |
474 | fields.Str(), validate=validate.OneOf(["freddie", "roger"]), | |
542 | 475 | ), |
543 | default_in=None, | |
476 | location="query", | |
544 | 477 | name="body", |
545 | 478 | ), |
546 | 479 | ] |
547 | + openapi.schema2parameters(PageSchema, default_in="query"), | |
480 | + openapi.schema2parameters(PageSchema, location="query"), | |
548 | 481 | "responses": { |
549 | 482 | 200: { |
550 | 483 | "description": "success", |
585 | 518 | class ValidationSchema(Schema): |
586 | 519 | id = fields.Int(dump_only=True) |
587 | 520 | range = fields.Int(validate=validate.Range(min=1, max=10)) |
521 | range_no_upper = fields.Float(validate=validate.Range(min=1)) | |
588 | 522 | multiple_ranges = fields.Int( |
589 | 523 | validate=[ |
590 | 524 | validate.Range(min=1), |
610 | 544 | equal_length = fields.Str( |
611 | 545 | validate=[validate.Length(equal=5), validate.Length(min=1, max=10)] |
612 | 546 | ) |
547 | date_range = fields.DateTime(validate=validate.Range(min=datetime(1900, 1, 1),)) | |
613 | 548 | |
614 | 549 | @pytest.mark.parametrize( |
615 | 550 | ("field", "properties"), |
616 | 551 | [ |
617 | 552 | ("range", {"minimum": 1, "maximum": 10}), |
553 | ("range_no_upper", {"minimum": 1}), | |
618 | 554 | ("multiple_ranges", {"minimum": 3, "maximum": 7}), |
619 | 555 | ("list_length", {"minItems": 1, "maxItems": 10}), |
620 | 556 | ("custom_list_length", {"minItems": 1, "maxItems": 10}), |
622 | 558 | ("custom_field_length", {"minLength": 1, "maxLength": 10}), |
623 | 559 | ("multiple_lengths", {"minLength": 3, "maxLength": 7}), |
624 | 560 | ("equal_length", {"minLength": 5, "maxLength": 5}), |
561 | ("date_range", {"x-minimum": datetime(1900, 1, 1)}), | |
625 | 562 | ], |
626 | 563 | ) |
627 | 564 | def test_properties(self, field, properties, spec): |
25 | 25 | @pytest.mark.parametrize("docstring", (None, "", "---")) |
26 | 26 | def test_load_operations_from_docstring_empty_docstring(docstring): |
27 | 27 | assert yaml_utils.load_operations_from_docstring(docstring) == {} |
28 | ||
29 | ||
30 | def test_dict_to_yaml_unicode(): | |
31 | assert yaml_utils.dict_to_yaml({"가": "나"}) == '"\\uAC00": "\\uB098"\n' | |
32 | assert yaml_utils.dict_to_yaml({"가": "나"}, {"allow_unicode": True}) == "가: 나\n" |
20 | 20 | return spec.to_dict()["components"]["parameters"] |
21 | 21 | |
22 | 22 | |
23 | def get_headers(spec): | |
24 | if spec.openapi_version.major < 3: | |
25 | return spec.to_dict()["headers"] | |
26 | return spec.to_dict()["components"]["headers"] | |
27 | ||
28 | ||
23 | 29 | def get_examples(spec): |
24 | 30 | return spec.to_dict()["components"]["examples"] |
25 | 31 |
0 | 0 | [tox] |
1 | 1 | envlist= |
2 | 2 | lint |
3 | py{35,36,37,38}-marshmallow2 | |
4 | py{35,36,37,38}-marshmallow3 | |
3 | py{36,37,38,39}-marshmallow3 | |
5 | 4 | py38-marshmallowdev |
6 | 5 | docs |
7 | 6 | |
8 | 7 | [testenv] |
9 | 8 | extras = tests |
10 | 9 | deps = |
11 | marshmallow2: marshmallow>=2.0.0,<3.0.0 | |
12 | 10 | marshmallow3: marshmallow>=3.0.0,<4.0.0 |
13 | 11 | marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz |
14 | 12 | commands = pytest {posargs} |
27 | 25 | [testenv:watch-docs] |
28 | 26 | deps = sphinx-autobuild |
29 | 27 | extras = docs |
30 | commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z src/apispec -s 2 | |
28 | commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/apispec --delay 2 | |
31 | 29 | |
32 | 30 | [testenv:watch-readme] |
33 | 31 | deps = restview |