schemathesis 3.13.0__py3-none-any.whl → 4.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1016
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +753 -74
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -1,215 +0,0 @@
1
- import pathlib
2
- from typing import IO, Any, Callable, Dict, Optional, Union, cast
3
-
4
- import graphql
5
- import requests
6
- from graphql import ExecutionResult
7
- from starlette.applications import Starlette
8
- from starlette.testclient import TestClient as ASGIClient
9
- from werkzeug import Client
10
- from yarl import URL
11
-
12
- from ...constants import DEFAULT_DATA_GENERATION_METHODS, CodeSampleStyle
13
- from ...exceptions import HTTPError
14
- from ...hooks import HookContext, dispatch
15
- from ...types import DataGenerationMethodInput, PathLike
16
- from ...utils import WSGIResponse, prepare_data_generation_methods, require_relative_url, setup_headers
17
- from .schemas import GraphQLSchema
18
-
19
- INTROSPECTION_QUERY = graphql.get_introspection_query()
20
- INTROSPECTION_QUERY_AST = graphql.parse(INTROSPECTION_QUERY)
21
-
22
-
23
- def from_path(
24
- path: PathLike,
25
- *,
26
- app: Any = None,
27
- base_url: Optional[str] = None,
28
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
29
- code_sample_style: str = CodeSampleStyle.default().name,
30
- encoding: str = "utf8",
31
- ) -> GraphQLSchema:
32
- """Load GraphQL schema via a file from an OS path.
33
-
34
- :param path: A path to the schema file.
35
- :param encoding: The name of the encoding used to decode the file.
36
- """
37
- with open(path, encoding=encoding) as fd:
38
- return from_file(
39
- fd,
40
- app=app,
41
- base_url=base_url,
42
- data_generation_methods=data_generation_methods,
43
- code_sample_style=code_sample_style,
44
- location=pathlib.Path(path).absolute().as_uri(),
45
- )
46
-
47
-
48
- def from_url(
49
- url: str,
50
- *,
51
- app: Any = None,
52
- base_url: Optional[str] = None,
53
- port: Optional[int] = None,
54
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
55
- code_sample_style: str = CodeSampleStyle.default().name,
56
- **kwargs: Any,
57
- ) -> GraphQLSchema:
58
- """Load GraphQL schema from the network.
59
-
60
- :param url: Schema URL.
61
- :param Optional[str] base_url: Base URL to send requests to.
62
- :param Optional[int] port: An optional port if you don't want to pass the ``base_url`` parameter, but only to change
63
- port in ``url``.
64
- :param app: A WSGI app instance.
65
- :return: GraphQLSchema
66
- """
67
- setup_headers(kwargs)
68
- kwargs.setdefault("json", {"query": INTROSPECTION_QUERY})
69
- if not base_url and port:
70
- base_url = str(URL(url).with_port(port))
71
- response = requests.post(url, **kwargs)
72
- HTTPError.raise_for_status(response)
73
- decoded = response.json()
74
- return from_dict(
75
- raw_schema=decoded["data"],
76
- location=url,
77
- base_url=base_url,
78
- app=app,
79
- data_generation_methods=data_generation_methods,
80
- code_sample_style=code_sample_style,
81
- )
82
-
83
-
84
- def from_file(
85
- file: Union[IO[str], str],
86
- *,
87
- app: Any = None,
88
- base_url: Optional[str] = None,
89
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
90
- code_sample_style: str = CodeSampleStyle.default().name,
91
- location: Optional[str] = None,
92
- ) -> GraphQLSchema:
93
- """Load GraphQL schema from a file descriptor or a string.
94
-
95
- :param file: Could be a file descriptor, string or bytes.
96
- """
97
- if isinstance(file, str):
98
- data = file
99
- else:
100
- data = file.read()
101
- document = graphql.build_schema(data)
102
- result = graphql.execute(document, INTROSPECTION_QUERY_AST)
103
- # TYPES: We don't pass `is_awaitable` above, therefore `result` is of the `ExecutionResult` type
104
- result = cast(ExecutionResult, result)
105
- # TYPES:
106
- # - `document` is a valid schema, because otherwise `build_schema` will rise an error;
107
- # - `INTROSPECTION_QUERY` is a valid query - it is known upfront;
108
- # Therefore the execution result is always valid at this point and `result.data` is not `None`
109
- raw_schema = cast(Dict[str, Any], result.data)
110
- return from_dict(
111
- raw_schema,
112
- app=app,
113
- base_url=base_url,
114
- data_generation_methods=data_generation_methods,
115
- code_sample_style=code_sample_style,
116
- location=location,
117
- )
118
-
119
-
120
- def from_dict(
121
- raw_schema: Dict[str, Any],
122
- *,
123
- app: Any = None,
124
- base_url: Optional[str] = None,
125
- location: Optional[str] = None,
126
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
127
- code_sample_style: str = CodeSampleStyle.default().name,
128
- ) -> GraphQLSchema:
129
- """Load GraphQL schema from a Python dictionary.
130
-
131
- :param dict raw_schema: A schema to load.
132
- :param Optional[str] location: Optional schema location. Either a full URL or a filesystem path.
133
- :param Optional[str] base_url: Base URL to send requests to.
134
- :param app: A WSGI app instance.
135
- :return: GraphQLSchema
136
- """
137
- _code_sample_style = CodeSampleStyle.from_str(code_sample_style)
138
- dispatch("before_load_schema", HookContext(), raw_schema)
139
- return GraphQLSchema(
140
- raw_schema,
141
- location=location,
142
- base_url=base_url,
143
- app=app,
144
- data_generation_methods=prepare_data_generation_methods(data_generation_methods),
145
- code_sample_style=_code_sample_style,
146
- ) # type: ignore
147
-
148
-
149
- def from_wsgi(
150
- schema_path: str,
151
- app: Any,
152
- *,
153
- base_url: Optional[str] = None,
154
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
155
- code_sample_style: str = CodeSampleStyle.default().name,
156
- **kwargs: Any,
157
- ) -> GraphQLSchema:
158
- """Load GraphQL schema from a WSGI app.
159
-
160
- :param str schema_path: An in-app relative URL to the schema.
161
- :param app: A WSGI app instance.
162
- :param Optional[str] base_url: Base URL to send requests to.
163
- :return: GraphQLSchema
164
- """
165
- require_relative_url(schema_path)
166
- setup_headers(kwargs)
167
- kwargs.setdefault("json", {"query": INTROSPECTION_QUERY})
168
- client = Client(app, WSGIResponse)
169
- response = client.post(schema_path, **kwargs)
170
- HTTPError.check_response(response, schema_path)
171
- return from_dict(
172
- raw_schema=response.json["data"],
173
- location=schema_path,
174
- base_url=base_url,
175
- app=app,
176
- data_generation_methods=data_generation_methods,
177
- code_sample_style=code_sample_style,
178
- )
179
-
180
-
181
- def from_asgi(
182
- schema_path: str,
183
- app: Any,
184
- *,
185
- base_url: Optional[str] = None,
186
- data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
187
- code_sample_style: str = CodeSampleStyle.default().name,
188
- **kwargs: Any,
189
- ) -> GraphQLSchema:
190
- """Load GraphQL schema from an ASGI app.
191
-
192
- :param str schema_path: An in-app relative URL to the schema.
193
- :param app: An ASGI app instance.
194
- :param Optional[str] base_url: Base URL to send requests to.
195
- """
196
- require_relative_url(schema_path)
197
- setup_headers(kwargs)
198
- kwargs.setdefault("json", {"query": INTROSPECTION_QUERY})
199
- client = ASGIClient(app)
200
- response = client.post(schema_path, **kwargs)
201
- HTTPError.check_response(response, schema_path)
202
- return from_dict(
203
- response.json()["data"],
204
- location=schema_path,
205
- base_url=base_url,
206
- app=app,
207
- data_generation_methods=data_generation_methods,
208
- code_sample_style=code_sample_style,
209
- )
210
-
211
-
212
- def get_loader_for_app(app: Any) -> Callable:
213
- if isinstance(app, Starlette):
214
- return from_asgi
215
- return from_wsgi
@@ -1,7 +0,0 @@
1
- LOCATION_TO_CONTAINER = {
2
- "path": "path_parameters",
3
- "query": "query",
4
- "header": "headers",
5
- "cookie": "cookies",
6
- "body": "body",
7
- }
@@ -1,12 +0,0 @@
1
- import attr
2
-
3
- from ....models import Case
4
- from ....utils import GenericResponse
5
-
6
-
7
- @attr.s(slots=True) # pragma: no mutate
8
- class ExpressionContext:
9
- """Context in what an expression are evaluated."""
10
-
11
- response: GenericResponse = attr.ib() # pragma: no mutate
12
- case: Case = attr.ib() # pragma: no mutate
@@ -1,29 +0,0 @@
1
- from typing import Any, Dict, List, Optional, Union
2
-
3
-
4
- def resolve(document: Any, pointer: str) -> Optional[Union[Dict, List, str, int, float]]:
5
- """Implementation is adapted from Rust's `serde-json` crate.
6
-
7
- Ref: https://github.com/serde-rs/json/blob/master/src/value/mod.rs#L751
8
- """
9
- if not pointer:
10
- return document
11
- if not pointer.startswith("/"):
12
- return None
13
-
14
- def replace(value: str) -> str:
15
- return value.replace("~1", "/").replace("~0", "~")
16
-
17
- tokens = map(replace, pointer.split("/")[1:])
18
- target = document
19
- for token in tokens:
20
- if isinstance(target, dict):
21
- target = target.get(token)
22
- elif isinstance(target, list):
23
- try:
24
- target = target[int(token)]
25
- except IndexError:
26
- return None
27
- else:
28
- return None
29
- return target
@@ -1,44 +0,0 @@
1
- import re
2
- from typing import List, Optional
3
-
4
- from ...types import Filter
5
- from ...utils import force_tuple
6
-
7
-
8
- def should_skip_method(method: str, pattern: Optional[Filter]) -> bool:
9
- if pattern is None:
10
- return False
11
- patterns = force_tuple(pattern)
12
- return method.upper() not in map(str.upper, patterns)
13
-
14
-
15
- def should_skip_endpoint(endpoint: str, pattern: Optional[Filter]) -> bool:
16
- if pattern is None:
17
- return False
18
- return not _match_any_pattern(endpoint, pattern)
19
-
20
-
21
- def should_skip_by_tag(tags: Optional[List[str]], pattern: Optional[Filter]) -> bool:
22
- if pattern is None:
23
- return False
24
- if not tags:
25
- return True
26
- patterns = force_tuple(pattern)
27
- return not any(re.search(item, tag) for item in patterns for tag in tags)
28
-
29
-
30
- def should_skip_by_operation_id(operation_id: Optional[str], pattern: Optional[Filter]) -> bool:
31
- if pattern is None:
32
- return False
33
- if not operation_id:
34
- return True
35
- return not _match_any_pattern(operation_id, pattern)
36
-
37
-
38
- def should_skip_deprecated(is_deprecated: bool, skip_deprecated_operations: bool) -> bool:
39
- return skip_deprecated_operations and is_deprecated
40
-
41
-
42
- def _match_any_pattern(target: str, pattern: Filter) -> bool:
43
- patterns = force_tuple(pattern)
44
- return any(re.search(item, target) for item in patterns)
@@ -1,302 +0,0 @@
1
- """Open API links support.
2
-
3
- Based on https://swagger.io/docs/specification/links/
4
- """
5
- from copy import deepcopy
6
- from difflib import get_close_matches
7
- from typing import Any, Dict, Generator, List, NoReturn, Optional, Sequence, Tuple, Union
8
-
9
- import attr
10
-
11
- from ...models import APIOperation, Case
12
- from ...parameters import ParameterSet
13
- from ...stateful import Direction, ParsedData, StatefulTest
14
- from ...types import NotSet
15
- from ...utils import NOT_SET, GenericResponse
16
- from . import expressions
17
- from .constants import LOCATION_TO_CONTAINER
18
- from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter
19
-
20
-
21
- @attr.s(slots=True, repr=False) # pragma: no mutate
22
- class Link(StatefulTest):
23
- operation: APIOperation = attr.ib() # pragma: no mutate
24
- parameters: Dict[str, Any] = attr.ib() # pragma: no mutate
25
- request_body: Any = attr.ib(default=NOT_SET) # pragma: no mutate
26
-
27
- @request_body.validator
28
- def is_defined(self, attribute: attr.Attribute, value: Any) -> None:
29
- if value is not NOT_SET and not self.operation.body:
30
- # Link defines `requestBody` for a parameter that does not accept one
31
- raise ValueError(
32
- f"Request body is not defined in API operation {self.operation.method.upper()} {self.operation.path}"
33
- )
34
-
35
- @classmethod
36
- def from_definition(
37
- cls, name: str, definition: Dict[str, Dict[str, Any]], source_operation: APIOperation
38
- ) -> "Link":
39
- # Links can be behind a reference
40
- _, definition = source_operation.schema.resolver.resolve_in_scope( # type: ignore
41
- definition, source_operation.definition.scope
42
- )
43
- if "operationId" in definition:
44
- # source_operation.schema is `BaseOpenAPISchema` and has this method
45
- operation = source_operation.schema.get_operation_by_id(definition["operationId"]) # type: ignore
46
- else:
47
- operation = source_operation.schema.get_operation_by_reference(definition["operationRef"]) # type: ignore
48
- return cls(
49
- # Pylint can't detect that the API operation is always defined at this point
50
- # E.g. if there is no matching operation or no operations at all, then a ValueError will be risen
51
- name=name,
52
- operation=operation, # pylint: disable=undefined-loop-variable
53
- parameters=definition.get("parameters", {}),
54
- request_body=definition.get("requestBody", NOT_SET), # `None` might be a valid value - `null`
55
- )
56
-
57
- def parse(self, case: Case, response: GenericResponse) -> ParsedData:
58
- """Parse data into a structure expected by links definition."""
59
- context = expressions.ExpressionContext(case=case, response=response)
60
- parameters = {
61
- parameter: expressions.evaluate(expression, context) for parameter, expression in self.parameters.items()
62
- }
63
- return ParsedData(
64
- parameters=parameters,
65
- # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#link-object
66
- # > A literal value or {expression} to use as a request body when calling the target operation.
67
- # In this case all literals will be passed as is, and expressions will be evaluated
68
- body=expressions.evaluate(self.request_body, context),
69
- )
70
-
71
- def make_operation(self, collected: List[ParsedData]) -> APIOperation:
72
- """Create a modified version of the original API operation with additional data merged in."""
73
- # We split the gathered data among all locations & store the original parameter
74
- containers = {
75
- location: {
76
- parameter.name: {"options": [], "parameter": parameter}
77
- for parameter in getattr(self.operation, container_name)
78
- }
79
- for location, container_name in LOCATION_TO_CONTAINER.items()
80
- }
81
- # There might be duplicates in the data
82
- for item in set(collected):
83
- for name, value in item.parameters.items():
84
- container = self._get_container_by_parameter_name(name, containers)
85
- container.append(value)
86
- if "body" in containers["body"] and item.body is not NOT_SET:
87
- containers["body"]["body"]["options"].append(item.body)
88
- # These are the final `path_parameters`, `query`, and other API operation components
89
- components: Dict[str, ParameterSet] = {
90
- container_name: getattr(self.operation, container_name).__class__()
91
- for location, container_name in LOCATION_TO_CONTAINER.items()
92
- }
93
- # Here are all components that are filled with parameters
94
- for location, parameters in containers.items():
95
- for name, parameter_data in parameters.items():
96
- parameter = parameter_data["parameter"]
97
- if parameter_data["options"]:
98
- definition = deepcopy(parameter.definition)
99
- if "schema" in definition:
100
- # The actual schema doesn't matter since we have a list of allowed values
101
- definition["schema"] = {"enum": parameter_data["options"]}
102
- else:
103
- # Other schema-related keywords will be ignored later, during the canonicalisation step
104
- # inside `hypothesis-jsonschema`
105
- definition["enum"] = parameter_data["options"]
106
- new_parameter: OpenAPIParameter
107
- if isinstance(parameter, OpenAPI30Body):
108
- new_parameter = parameter.__class__(
109
- definition, media_type=parameter.media_type, required=parameter.required
110
- )
111
- elif isinstance(parameter, OpenAPI20Body):
112
- new_parameter = parameter.__class__(definition, media_type=parameter.media_type)
113
- else:
114
- new_parameter = parameter.__class__(definition)
115
- components[LOCATION_TO_CONTAINER[location]].add(new_parameter)
116
- else:
117
- # No options were gathered for this parameter - use the original one
118
- components[LOCATION_TO_CONTAINER[location]].add(parameter)
119
- return self.operation.clone(**components)
120
-
121
- def _get_container_by_parameter_name(self, full_name: str, templates: Dict[str, Dict[str, Dict[str, Any]]]) -> List:
122
- """Detect in what request part the parameters is defined."""
123
- location: Optional[str]
124
- try:
125
- # The parameter name is prefixed with its location. Example: `path.id`
126
- location, name = full_name.split(".")
127
- except ValueError:
128
- location, name = None, full_name
129
- if location:
130
- try:
131
- parameters = templates[location]
132
- except KeyError:
133
- self._unknown_parameter(full_name)
134
- else:
135
- for parameters in templates.values():
136
- if name in parameters:
137
- break
138
- else:
139
- self._unknown_parameter(full_name)
140
- if not parameters:
141
- self._unknown_parameter(full_name)
142
- return parameters[name]["options"]
143
-
144
- def _unknown_parameter(self, name: str) -> NoReturn:
145
- raise ValueError(
146
- f"Parameter `{name}` is not defined in API operation {self.operation.method.upper()} {self.operation.path}"
147
- )
148
-
149
-
150
- def get_links(response: GenericResponse, operation: APIOperation, field: str) -> Sequence[Link]:
151
- """Get `x-links` / `links` definitions from the schema."""
152
- responses = operation.definition.resolved["responses"]
153
- if str(response.status_code) in responses:
154
- response_definition = responses[str(response.status_code)]
155
- elif response.status_code in responses:
156
- response_definition = responses[response.status_code]
157
- else:
158
- response_definition = responses.get("default", {})
159
- links = response_definition.get(field, {})
160
- return [Link.from_definition(name, definition, operation) for name, definition in links.items()]
161
-
162
-
163
- @attr.s(slots=True, repr=False) # pragma: no mutate
164
- class OpenAPILink(Direction):
165
- """Alternative approach to link processing.
166
-
167
- NOTE. This class will replace `Link` in the future.
168
- """
169
-
170
- name: str = attr.ib() # pragma: no mutate
171
- status_code: str = attr.ib() # pragma: no mutate
172
- definition: Dict[str, Any] = attr.ib() # pragma: no mutate
173
- operation: APIOperation = attr.ib() # pragma: no mutate
174
- parameters: List[Tuple[Optional[str], str, str]] = attr.ib(init=False) # pragma: no mutate
175
- body: Union[Dict[str, Any], NotSet] = attr.ib(init=False) # pragma: no mutate
176
-
177
- def __attrs_post_init__(self) -> None:
178
- self.parameters = [
179
- normalize_parameter(parameter, expression)
180
- for parameter, expression in self.definition.get("parameters", {}).items()
181
- ]
182
- self.body = self.definition.get("requestBody", NOT_SET)
183
-
184
- def set_data(self, case: Case, **kwargs: Any) -> None:
185
- """Assign all linked definitions to the new case instance."""
186
- context = kwargs["context"]
187
- self.set_parameters(case, context)
188
- self.set_body(case, context)
189
- case.set_source(context.response, context.case)
190
-
191
- def set_parameters(self, case: Case, context: expressions.ExpressionContext) -> None:
192
- for location, name, expression in self.parameters:
193
- container = get_container(case, location, name)
194
- # Might happen if there is directly specified container,
195
- # but the schema has no parameters of such type at all.
196
- # Therefore the container is empty, otherwise it will be at least an empty object
197
- if container is None:
198
- message = f"No such parameter in `{case.operation.method.upper()} {case.operation.path}`: `{name}`."
199
- possibilities = [param.name for param in case.operation.definition.parameters]
200
- matches = get_close_matches(name, possibilities)
201
- if matches:
202
- message += f" Did you mean `{matches[0]}`?"
203
- raise ValueError(message)
204
- container[name] = expressions.evaluate(expression, context)
205
-
206
- def set_body(self, case: Case, context: expressions.ExpressionContext) -> None:
207
- if self.body is not NOT_SET:
208
- case.body = expressions.evaluate(self.body, context)
209
-
210
- def get_target_operation(self) -> APIOperation:
211
- if "operationId" in self.definition:
212
- return self.operation.schema.get_operation_by_id(self.definition["operationId"]) # type: ignore
213
- return self.operation.schema.get_operation_by_reference(self.definition["operationRef"]) # type: ignore
214
-
215
-
216
- def get_container(case: Case, location: Optional[str], name: str) -> Optional[Dict[str, Any]]:
217
- """Get a container that suppose to store the given parameter."""
218
- if location:
219
- container_name = LOCATION_TO_CONTAINER[location]
220
- else:
221
- for param in case.operation.definition.parameters:
222
- if param.name == name:
223
- container_name = LOCATION_TO_CONTAINER[param.location]
224
- break
225
- else:
226
- raise ValueError(f"Parameter `{name}` is not defined in API operation `{case.operation.verbose_name}`")
227
- return getattr(case, container_name)
228
-
229
-
230
- def normalize_parameter(parameter: str, expression: str) -> Tuple[Optional[str], str, str]:
231
- """Normalize runtime expressions.
232
-
233
- Runtime expressions may have parameter names prefixed with their location - `path.id`.
234
- At the same time, parameters could be defined without a prefix - `id`.
235
- We need to normalize all parameters to the same form to simplify working with them.
236
- """
237
- try:
238
- # The parameter name is prefixed with its location. Example: `path.id`
239
- location, name = tuple(parameter.split("."))
240
- return location, name, expression
241
- except ValueError:
242
- return None, parameter, expression
243
-
244
-
245
- def get_all_links(operation: APIOperation) -> Generator[Tuple[str, OpenAPILink], None, None]:
246
- for status_code, definition in operation.definition.resolved["responses"].items():
247
- for name, link_definition in definition.get(operation.schema.links_field, {}).items(): # type: ignore
248
- yield status_code, OpenAPILink(name, status_code, link_definition, operation)
249
-
250
-
251
- StatusCode = Union[str, int]
252
-
253
-
254
- def _get_response_by_status_code(responses: Dict[StatusCode, Dict[str, Any]], status_code: Union[str, int]) -> Dict:
255
- if isinstance(status_code, int):
256
- # Invalid schemas may contain status codes as integers
257
- if status_code in responses:
258
- return responses[status_code]
259
- # Passed here as an integer, but there is no such status code as int
260
- # We cast it to a string because it is either there already and we'll get relevant responses, otherwise
261
- # a new dict will be created because there is no such status code in the schema (as an int or a string)
262
- return responses.setdefault(str(status_code), {})
263
- if status_code.isnumeric():
264
- # Invalid schema but the status code is passed as a string
265
- numeric_status_code = int(status_code)
266
- if numeric_status_code in responses:
267
- return responses[numeric_status_code]
268
- # All status codes as strings, including `default` and patterned values like `5XX`
269
- return responses.setdefault(status_code, {})
270
-
271
-
272
- def add_link(
273
- responses: Dict[StatusCode, Dict[str, Any]],
274
- links_field: str,
275
- parameters: Optional[Dict[str, str]],
276
- request_body: Any,
277
- status_code: StatusCode,
278
- target: Union[str, APIOperation],
279
- ) -> None:
280
- response = _get_response_by_status_code(responses, status_code)
281
- links_definition = response.setdefault(links_field, {})
282
- new_link: Dict[str, Union[str, Dict[str, str]]] = {}
283
- if parameters is not None:
284
- new_link["parameters"] = parameters
285
- if request_body is not None:
286
- new_link["requestBody"] = request_body
287
- if isinstance(target, str):
288
- name = target
289
- new_link["operationRef"] = target
290
- else:
291
- name = f"{target.method.upper()} {target.path}"
292
- # operationId is a dict lookup which is more efficient than using `operationRef`, since it
293
- # doesn't involve reference resolving when we will look up for this target during testing.
294
- if "operationId" in target.definition.resolved:
295
- new_link["operationId"] = target.definition.resolved["operationId"]
296
- else:
297
- new_link["operationRef"] = target.operation_reference
298
- # The name is arbitrary, so we don't really case what it is,
299
- # but it should not override existing links
300
- while name in links_definition:
301
- name += "_new"
302
- links_definition[name] = new_link