schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +26 -68
- schemathesis/checks.py +130 -60
- schemathesis/cli/__init__.py +5 -2105
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +30 -0
- schemathesis/cli/commands/run/executor.py +141 -0
- schemathesis/cli/commands/run/filters.py +202 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +1368 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +37 -16
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +243 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +49 -68
- schemathesis/engine/phases/stateful/__init__.py +66 -0
- schemathesis/engine/phases/stateful/_executor.py +301 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +175 -0
- schemathesis/engine/phases/unit/_executor.py +322 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +246 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +9 -40
- schemathesis/filters.py +7 -95
- schemathesis/generation/__init__.py +3 -3
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +22 -22
- schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +17 -62
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +387 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +456 -228
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +5 -3
- schemathesis/specs/graphql/schemas.py +122 -123
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +97 -134
- schemathesis/specs/openapi/checks.py +238 -219
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +22 -20
- schemathesis/specs/openapi/expressions/__init__.py +11 -15
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/nodes.py +33 -32
- schemathesis/specs/openapi/formats.py +3 -2
- schemathesis/specs/openapi/links.py +123 -299
- schemathesis/specs/openapi/media_types.py +10 -12
- schemathesis/specs/openapi/negative/__init__.py +2 -1
- schemathesis/specs/openapi/negative/mutations.py +3 -2
- schemathesis/specs/openapi/parameters.py +8 -6
- schemathesis/specs/openapi/patterns.py +1 -1
- schemathesis/specs/openapi/references.py +11 -51
- schemathesis/specs/openapi/schemas.py +177 -191
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +10 -6
- schemathesis/specs/openapi/stateful/__init__.py +97 -91
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -7
- schemathesis/transport/wsgi.py +165 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
- schemathesis-4.0.0a2.dist-info/RECORD +151 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -559
- schemathesis/_override.py +0 -50
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -936
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -56
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -277
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -84
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -38
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -104
- schemathesis/runner/impl/core.py +0 -1246
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/loaders.py +0 -708
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -359
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.7.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,708 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import io
|
4
|
-
import json
|
5
|
-
import pathlib
|
6
|
-
import re
|
7
|
-
from typing import IO, TYPE_CHECKING, Any, Callable, cast
|
8
|
-
from urllib.parse import urljoin
|
9
|
-
|
10
|
-
from ... import experimental, fixups
|
11
|
-
from ...code_samples import CodeSampleStyle
|
12
|
-
from ...constants import DEFAULT_RESPONSE_TIMEOUT, NOT_SET, WAIT_FOR_SCHEMA_INTERVAL
|
13
|
-
from ...exceptions import SchemaError, SchemaErrorType
|
14
|
-
from ...filters import filter_set_from_components
|
15
|
-
from ...generation import (
|
16
|
-
DEFAULT_DATA_GENERATION_METHODS,
|
17
|
-
DataGenerationMethod,
|
18
|
-
DataGenerationMethodInput,
|
19
|
-
GenerationConfig,
|
20
|
-
)
|
21
|
-
from ...hooks import HookContext, dispatch
|
22
|
-
from ...internal.deprecation import warn_filtration_arguments
|
23
|
-
from ...internal.output import OutputConfig
|
24
|
-
from ...internal.validation import require_relative_url
|
25
|
-
from ...loaders import load_schema_from_url, load_yaml
|
26
|
-
from ...throttling import build_limiter
|
27
|
-
from ...transports.content_types import is_json_media_type, is_yaml_media_type
|
28
|
-
from ...transports.headers import setup_default_headers
|
29
|
-
from ...types import Filter, NotSet, PathLike, Specification
|
30
|
-
from . import definitions, validation
|
31
|
-
|
32
|
-
if TYPE_CHECKING:
|
33
|
-
import jsonschema
|
34
|
-
from pyrate_limiter import Limiter
|
35
|
-
|
36
|
-
from ...lazy import LazySchema
|
37
|
-
from ...transports.responses import GenericResponse
|
38
|
-
from .schemas import BaseOpenAPISchema
|
39
|
-
|
40
|
-
|
41
|
-
def _is_json_response(response: GenericResponse) -> bool:
|
42
|
-
"""Guess if the response contains JSON."""
|
43
|
-
content_type = response.headers.get("Content-Type")
|
44
|
-
if content_type is not None:
|
45
|
-
return is_json_media_type(content_type)
|
46
|
-
return False
|
47
|
-
|
48
|
-
|
49
|
-
def _has_suffix(path: PathLike, suffix: str) -> bool:
|
50
|
-
if isinstance(path, str):
|
51
|
-
return path.endswith(suffix)
|
52
|
-
return path.suffix == suffix
|
53
|
-
|
54
|
-
|
55
|
-
def _is_json_path(path: PathLike) -> bool:
|
56
|
-
return _has_suffix(path, ".json")
|
57
|
-
|
58
|
-
|
59
|
-
def _is_yaml_response(response: GenericResponse) -> bool:
|
60
|
-
"""Guess if the response contains YAML."""
|
61
|
-
content_type = response.headers.get("Content-Type")
|
62
|
-
if content_type is not None:
|
63
|
-
return is_yaml_media_type(content_type)
|
64
|
-
return False
|
65
|
-
|
66
|
-
|
67
|
-
def _is_yaml_path(path: PathLike) -> bool:
|
68
|
-
return _has_suffix(path, ".yaml") or _has_suffix(path, ".yml")
|
69
|
-
|
70
|
-
|
71
|
-
def from_path(
|
72
|
-
path: PathLike,
|
73
|
-
*,
|
74
|
-
app: Any = None,
|
75
|
-
base_url: str | None = None,
|
76
|
-
method: Filter | None = None,
|
77
|
-
endpoint: Filter | None = None,
|
78
|
-
tag: Filter | None = None,
|
79
|
-
operation_id: Filter | None = None,
|
80
|
-
skip_deprecated_operations: bool | None = None,
|
81
|
-
validate_schema: bool = False,
|
82
|
-
force_schema_version: str | None = None,
|
83
|
-
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
84
|
-
generation_config: GenerationConfig | None = None,
|
85
|
-
output_config: OutputConfig | None = None,
|
86
|
-
code_sample_style: str = CodeSampleStyle.default().name,
|
87
|
-
rate_limit: str | None = None,
|
88
|
-
encoding: str = "utf8",
|
89
|
-
sanitize_output: bool = True,
|
90
|
-
) -> BaseOpenAPISchema:
|
91
|
-
"""Load Open API schema via a file from an OS path.
|
92
|
-
|
93
|
-
:param path: A path to the schema file.
|
94
|
-
:param encoding: The name of the encoding used to decode the file.
|
95
|
-
"""
|
96
|
-
with open(path, encoding=encoding) as fd:
|
97
|
-
return from_file(
|
98
|
-
fd,
|
99
|
-
app=app,
|
100
|
-
base_url=base_url,
|
101
|
-
method=method,
|
102
|
-
endpoint=endpoint,
|
103
|
-
tag=tag,
|
104
|
-
operation_id=operation_id,
|
105
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
106
|
-
validate_schema=validate_schema,
|
107
|
-
force_schema_version=force_schema_version,
|
108
|
-
data_generation_methods=data_generation_methods,
|
109
|
-
generation_config=generation_config,
|
110
|
-
output_config=output_config,
|
111
|
-
code_sample_style=code_sample_style,
|
112
|
-
location=pathlib.Path(path).absolute().as_uri(),
|
113
|
-
rate_limit=rate_limit,
|
114
|
-
sanitize_output=sanitize_output,
|
115
|
-
__expects_json=_is_json_path(path),
|
116
|
-
__expects_yaml=_is_yaml_path(path),
|
117
|
-
)
|
118
|
-
|
119
|
-
|
120
|
-
def from_uri(
|
121
|
-
uri: str,
|
122
|
-
*,
|
123
|
-
app: Any = None,
|
124
|
-
base_url: str | None = None,
|
125
|
-
port: int | None = None,
|
126
|
-
method: Filter | None = None,
|
127
|
-
endpoint: Filter | None = None,
|
128
|
-
tag: Filter | None = None,
|
129
|
-
operation_id: Filter | None = None,
|
130
|
-
skip_deprecated_operations: bool | None = None,
|
131
|
-
validate_schema: bool = False,
|
132
|
-
force_schema_version: str | None = None,
|
133
|
-
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
134
|
-
generation_config: GenerationConfig | None = None,
|
135
|
-
output_config: OutputConfig | None = None,
|
136
|
-
code_sample_style: str = CodeSampleStyle.default().name,
|
137
|
-
wait_for_schema: float | None = None,
|
138
|
-
rate_limit: str | None = None,
|
139
|
-
sanitize_output: bool = True,
|
140
|
-
**kwargs: Any,
|
141
|
-
) -> BaseOpenAPISchema:
|
142
|
-
"""Load Open API schema from the network.
|
143
|
-
|
144
|
-
:param str uri: Schema URL.
|
145
|
-
"""
|
146
|
-
import backoff
|
147
|
-
import requests
|
148
|
-
|
149
|
-
setup_default_headers(kwargs)
|
150
|
-
if port:
|
151
|
-
from yarl import URL
|
152
|
-
|
153
|
-
uri = str(URL(uri).with_port(port))
|
154
|
-
if not base_url:
|
155
|
-
base_url = uri
|
156
|
-
|
157
|
-
if wait_for_schema is not None:
|
158
|
-
|
159
|
-
@backoff.on_exception( # type: ignore
|
160
|
-
backoff.constant,
|
161
|
-
requests.exceptions.ConnectionError,
|
162
|
-
max_time=wait_for_schema,
|
163
|
-
interval=WAIT_FOR_SCHEMA_INTERVAL,
|
164
|
-
)
|
165
|
-
def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
|
166
|
-
return requests.get(_uri, **_kwargs)
|
167
|
-
|
168
|
-
else:
|
169
|
-
_load_schema = requests.get
|
170
|
-
|
171
|
-
kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
|
172
|
-
response = load_schema_from_url(lambda: _load_schema(uri, **kwargs))
|
173
|
-
return from_file(
|
174
|
-
response.text,
|
175
|
-
app=app,
|
176
|
-
base_url=base_url,
|
177
|
-
method=method,
|
178
|
-
endpoint=endpoint,
|
179
|
-
tag=tag,
|
180
|
-
operation_id=operation_id,
|
181
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
182
|
-
validate_schema=validate_schema,
|
183
|
-
force_schema_version=force_schema_version,
|
184
|
-
data_generation_methods=data_generation_methods,
|
185
|
-
generation_config=generation_config,
|
186
|
-
output_config=output_config,
|
187
|
-
code_sample_style=code_sample_style,
|
188
|
-
location=uri,
|
189
|
-
rate_limit=rate_limit,
|
190
|
-
sanitize_output=sanitize_output,
|
191
|
-
__expects_json=_is_json_response(response),
|
192
|
-
__expects_yaml=_is_yaml_response(response),
|
193
|
-
)
|
194
|
-
|
195
|
-
|
196
|
-
SCHEMA_INVALID_ERROR = "The provided API schema does not appear to be a valid OpenAPI schema"
|
197
|
-
SCHEMA_LOADING_ERROR = "Received unsupported content while expecting a JSON or YAML payload for Open API"
|
198
|
-
SCHEMA_SYNTAX_ERROR = "API schema does not appear syntactically valid"
|
199
|
-
|
200
|
-
|
201
|
-
def _load_yaml(data: str, include_details_on_error: bool = False) -> dict[str, Any]:
|
202
|
-
import yaml
|
203
|
-
|
204
|
-
try:
|
205
|
-
return load_yaml(data)
|
206
|
-
except yaml.YAMLError as exc:
|
207
|
-
if include_details_on_error:
|
208
|
-
type_ = SchemaErrorType.SYNTAX_ERROR
|
209
|
-
message = SCHEMA_SYNTAX_ERROR
|
210
|
-
extras = [entry for entry in str(exc).splitlines() if entry]
|
211
|
-
else:
|
212
|
-
type_ = SchemaErrorType.UNEXPECTED_CONTENT_TYPE
|
213
|
-
message = SCHEMA_LOADING_ERROR
|
214
|
-
extras = []
|
215
|
-
raise SchemaError(type_, message, extras=extras) from exc
|
216
|
-
|
217
|
-
|
218
|
-
def from_file(
|
219
|
-
file: IO[str] | str,
|
220
|
-
*,
|
221
|
-
app: Any = None,
|
222
|
-
base_url: str | None = None,
|
223
|
-
method: Filter | None = None,
|
224
|
-
endpoint: Filter | None = None,
|
225
|
-
tag: Filter | None = None,
|
226
|
-
operation_id: Filter | None = None,
|
227
|
-
skip_deprecated_operations: bool | None = None,
|
228
|
-
validate_schema: bool = False,
|
229
|
-
force_schema_version: str | None = None,
|
230
|
-
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
231
|
-
generation_config: GenerationConfig | None = None,
|
232
|
-
output_config: OutputConfig | None = None,
|
233
|
-
code_sample_style: str = CodeSampleStyle.default().name,
|
234
|
-
location: str | None = None,
|
235
|
-
rate_limit: str | None = None,
|
236
|
-
sanitize_output: bool = True,
|
237
|
-
__expects_json: bool = False,
|
238
|
-
__expects_yaml: bool = False,
|
239
|
-
**kwargs: Any, # needed in the runner to have compatible API across all loaders
|
240
|
-
) -> BaseOpenAPISchema:
|
241
|
-
"""Load Open API schema from a file descriptor, string or bytes.
|
242
|
-
|
243
|
-
:param file: Could be a file descriptor, string or bytes.
|
244
|
-
"""
|
245
|
-
if hasattr(file, "read"):
|
246
|
-
data = file.read() # type: ignore
|
247
|
-
else:
|
248
|
-
data = file
|
249
|
-
if __expects_json:
|
250
|
-
try:
|
251
|
-
raw = json.loads(data)
|
252
|
-
except json.JSONDecodeError as exc:
|
253
|
-
# Fallback to a slower YAML loader. This way we'll still load schemas from responses with
|
254
|
-
# invalid `Content-Type` headers or YAML files that have the `.json` extension.
|
255
|
-
# This is a rare case, and it will be slower but trying JSON first improves a more common use case
|
256
|
-
try:
|
257
|
-
raw = _load_yaml(data)
|
258
|
-
except SchemaError:
|
259
|
-
raise SchemaError(
|
260
|
-
SchemaErrorType.SYNTAX_ERROR,
|
261
|
-
SCHEMA_SYNTAX_ERROR,
|
262
|
-
extras=[entry for entry in str(exc).splitlines() if entry],
|
263
|
-
) from exc
|
264
|
-
else:
|
265
|
-
raw = _load_yaml(data, include_details_on_error=__expects_yaml)
|
266
|
-
return from_dict(
|
267
|
-
raw,
|
268
|
-
app=app,
|
269
|
-
base_url=base_url,
|
270
|
-
method=method,
|
271
|
-
endpoint=endpoint,
|
272
|
-
tag=tag,
|
273
|
-
operation_id=operation_id,
|
274
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
275
|
-
validate_schema=validate_schema,
|
276
|
-
force_schema_version=force_schema_version,
|
277
|
-
data_generation_methods=data_generation_methods,
|
278
|
-
generation_config=generation_config,
|
279
|
-
output_config=output_config,
|
280
|
-
code_sample_style=code_sample_style,
|
281
|
-
location=location,
|
282
|
-
rate_limit=rate_limit,
|
283
|
-
sanitize_output=sanitize_output,
|
284
|
-
)
|
285
|
-
|
286
|
-
|
287
|
-
def _is_fast_api(app: Any) -> bool:
|
288
|
-
for cls in app.__class__.__mro__:
|
289
|
-
if f"{cls.__module__}.{cls.__qualname__}" == "fastapi.applications.FastAPI":
|
290
|
-
return True
|
291
|
-
return False
|
292
|
-
|
293
|
-
|
294
|
-
def from_dict(
|
295
|
-
raw_schema: dict[str, Any],
|
296
|
-
*,
|
297
|
-
app: Any = None,
|
298
|
-
base_url: str | None = None,
|
299
|
-
method: Filter | None = None,
|
300
|
-
endpoint: Filter | None = None,
|
301
|
-
tag: Filter | None = None,
|
302
|
-
operation_id: Filter | None = None,
|
303
|
-
skip_deprecated_operations: bool | None = None,
|
304
|
-
validate_schema: bool = False,
|
305
|
-
force_schema_version: str | None = None,
|
306
|
-
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
307
|
-
generation_config: GenerationConfig | None = None,
|
308
|
-
output_config: OutputConfig | None = None,
|
309
|
-
code_sample_style: str = CodeSampleStyle.default().name,
|
310
|
-
location: str | None = None,
|
311
|
-
rate_limit: str | None = None,
|
312
|
-
sanitize_output: bool = True,
|
313
|
-
) -> BaseOpenAPISchema:
|
314
|
-
"""Load Open API schema from a Python dictionary.
|
315
|
-
|
316
|
-
:param dict raw_schema: A schema to load.
|
317
|
-
"""
|
318
|
-
from ... import transports
|
319
|
-
from .schemas import OpenApi30, SwaggerV20
|
320
|
-
|
321
|
-
if not isinstance(raw_schema, dict):
|
322
|
-
raise SchemaError(SchemaErrorType.OPEN_API_INVALID_SCHEMA, SCHEMA_INVALID_ERROR)
|
323
|
-
_code_sample_style = CodeSampleStyle.from_str(code_sample_style)
|
324
|
-
hook_context = HookContext()
|
325
|
-
is_openapi_31 = raw_schema.get("openapi", "").startswith("3.1")
|
326
|
-
is_fast_api_fixup_installed = fixups.is_installed("fast_api")
|
327
|
-
if is_fast_api_fixup_installed and is_openapi_31:
|
328
|
-
fixups.fast_api.uninstall()
|
329
|
-
elif _is_fast_api(app):
|
330
|
-
fixups.fast_api.adjust_schema(raw_schema)
|
331
|
-
dispatch("before_load_schema", hook_context, raw_schema)
|
332
|
-
rate_limiter: Limiter | None = None
|
333
|
-
if rate_limit is not None:
|
334
|
-
rate_limiter = build_limiter(rate_limit)
|
335
|
-
|
336
|
-
for name in ("method", "endpoint", "tag", "operation_id", "skip_deprecated_operations"):
|
337
|
-
value = locals()[name]
|
338
|
-
if value is not None:
|
339
|
-
warn_filtration_arguments(name)
|
340
|
-
filter_set = filter_set_from_components(
|
341
|
-
include=True,
|
342
|
-
method=method,
|
343
|
-
endpoint=endpoint,
|
344
|
-
tag=tag,
|
345
|
-
operation_id=operation_id,
|
346
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
347
|
-
)
|
348
|
-
|
349
|
-
def init_openapi_2() -> SwaggerV20:
|
350
|
-
_maybe_validate_schema(raw_schema, definitions.SWAGGER_20_VALIDATOR, validate_schema)
|
351
|
-
instance = SwaggerV20(
|
352
|
-
raw_schema,
|
353
|
-
specification=Specification.OPENAPI,
|
354
|
-
app=app,
|
355
|
-
base_url=base_url,
|
356
|
-
filter_set=filter_set,
|
357
|
-
validate_schema=validate_schema,
|
358
|
-
data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
|
359
|
-
generation_config=generation_config or GenerationConfig(),
|
360
|
-
output_config=output_config or OutputConfig(),
|
361
|
-
code_sample_style=_code_sample_style,
|
362
|
-
location=location,
|
363
|
-
rate_limiter=rate_limiter,
|
364
|
-
sanitize_output=sanitize_output,
|
365
|
-
transport=transports.get(app),
|
366
|
-
)
|
367
|
-
dispatch("after_load_schema", hook_context, instance)
|
368
|
-
return instance
|
369
|
-
|
370
|
-
def init_openapi_3(forced: bool) -> OpenApi30:
|
371
|
-
version = raw_schema["openapi"]
|
372
|
-
if (
|
373
|
-
not (is_openapi_31 and experimental.OPEN_API_3_1.is_enabled)
|
374
|
-
and not forced
|
375
|
-
and not OPENAPI_30_VERSION_RE.match(version)
|
376
|
-
):
|
377
|
-
if is_openapi_31:
|
378
|
-
raise SchemaError(
|
379
|
-
SchemaErrorType.OPEN_API_EXPERIMENTAL_VERSION,
|
380
|
-
f"The provided schema uses Open API {version}, which is currently not fully supported.",
|
381
|
-
)
|
382
|
-
raise SchemaError(
|
383
|
-
SchemaErrorType.OPEN_API_UNSUPPORTED_VERSION,
|
384
|
-
f"The provided schema uses Open API {version}, which is currently not supported.",
|
385
|
-
)
|
386
|
-
if is_openapi_31:
|
387
|
-
validator = definitions.OPENAPI_31_VALIDATOR
|
388
|
-
else:
|
389
|
-
validator = definitions.OPENAPI_30_VALIDATOR
|
390
|
-
_maybe_validate_schema(raw_schema, validator, validate_schema)
|
391
|
-
instance = OpenApi30(
|
392
|
-
raw_schema,
|
393
|
-
specification=Specification.OPENAPI,
|
394
|
-
app=app,
|
395
|
-
base_url=base_url,
|
396
|
-
filter_set=filter_set,
|
397
|
-
validate_schema=validate_schema,
|
398
|
-
data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
|
399
|
-
generation_config=generation_config or GenerationConfig(),
|
400
|
-
output_config=output_config or OutputConfig(),
|
401
|
-
code_sample_style=_code_sample_style,
|
402
|
-
location=location,
|
403
|
-
rate_limiter=rate_limiter,
|
404
|
-
sanitize_output=sanitize_output,
|
405
|
-
transport=transports.get(app),
|
406
|
-
)
|
407
|
-
dispatch("after_load_schema", hook_context, instance)
|
408
|
-
return instance
|
409
|
-
|
410
|
-
if force_schema_version == "20":
|
411
|
-
return init_openapi_2()
|
412
|
-
if force_schema_version == "30":
|
413
|
-
return init_openapi_3(forced=True)
|
414
|
-
if "swagger" in raw_schema:
|
415
|
-
return init_openapi_2()
|
416
|
-
if "openapi" in raw_schema:
|
417
|
-
return init_openapi_3(forced=False)
|
418
|
-
raise SchemaError(
|
419
|
-
SchemaErrorType.OPEN_API_UNSPECIFIED_VERSION,
|
420
|
-
"Unable to determine the Open API version as it's not specified in the document.",
|
421
|
-
)
|
422
|
-
|
423
|
-
|
424
|
-
OPENAPI_30_VERSION_RE = re.compile(r"^3\.0\.\d(-.+)?$")
|
425
|
-
|
426
|
-
# It is a common case when API schemas are stored in the YAML format and HTTP status codes are numbers
|
427
|
-
# The Open API spec requires HTTP status codes as strings
|
428
|
-
DOC_ENTRY = "https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#patterned-fields-1"
|
429
|
-
NUMERIC_STATUS_CODES_MESSAGE = f"""Numeric HTTP status codes detected in your YAML schema.
|
430
|
-
According to the Open API specification, status codes must be strings, not numbers.
|
431
|
-
For more details, check the Open API documentation: {DOC_ENTRY}
|
432
|
-
|
433
|
-
Please, stringify the following status codes:"""
|
434
|
-
NON_STRING_OBJECT_KEY_MESSAGE = (
|
435
|
-
"The Open API specification requires all keys in the schema to be strings. "
|
436
|
-
"You have some keys that are not strings."
|
437
|
-
)
|
438
|
-
|
439
|
-
|
440
|
-
def _format_status_codes(status_codes: list[tuple[int, list[str | int]]]) -> str:
|
441
|
-
buffer = io.StringIO()
|
442
|
-
for status_code, path in status_codes:
|
443
|
-
buffer.write(f" - {status_code} at schema['paths']")
|
444
|
-
for chunk in path:
|
445
|
-
buffer.write(f"[{chunk!r}]")
|
446
|
-
buffer.write("['responses']\n")
|
447
|
-
return buffer.getvalue().rstrip()
|
448
|
-
|
449
|
-
|
450
|
-
def _maybe_validate_schema(
|
451
|
-
instance: dict[str, Any], validator: jsonschema.validators.Draft4Validator, validate_schema: bool
|
452
|
-
) -> None:
|
453
|
-
from jsonschema import ValidationError
|
454
|
-
|
455
|
-
if validate_schema:
|
456
|
-
try:
|
457
|
-
validator.validate(instance)
|
458
|
-
except TypeError as exc:
|
459
|
-
if validation.is_pattern_error(exc):
|
460
|
-
status_codes = validation.find_numeric_http_status_codes(instance)
|
461
|
-
if status_codes:
|
462
|
-
message = _format_status_codes(status_codes)
|
463
|
-
raise SchemaError(
|
464
|
-
SchemaErrorType.YAML_NUMERIC_STATUS_CODES, f"{NUMERIC_STATUS_CODES_MESSAGE}\n{message}"
|
465
|
-
) from exc
|
466
|
-
# Some other pattern error
|
467
|
-
raise SchemaError(SchemaErrorType.YAML_NON_STRING_KEYS, NON_STRING_OBJECT_KEY_MESSAGE) from exc
|
468
|
-
raise SchemaError(SchemaErrorType.UNCLASSIFIED, "Unknown error") from exc
|
469
|
-
except ValidationError as exc:
|
470
|
-
raise SchemaError(
|
471
|
-
SchemaErrorType.OPEN_API_INVALID_SCHEMA,
|
472
|
-
SCHEMA_INVALID_ERROR,
|
473
|
-
extras=[entry for entry in str(exc).splitlines() if entry],
|
474
|
-
) from exc
|
475
|
-
|
476
|
-
|
477
|
-
def from_pytest_fixture(
|
478
|
-
fixture_name: str,
|
479
|
-
*,
|
480
|
-
app: Any = NOT_SET,
|
481
|
-
base_url: str | None | NotSet = NOT_SET,
|
482
|
-
method: Filter | None = NOT_SET,
|
483
|
-
endpoint: Filter | None = NOT_SET,
|
484
|
-
tag: Filter | None = NOT_SET,
|
485
|
-
operation_id: Filter | None = NOT_SET,
|
486
|
-
skip_deprecated_operations: bool | None = None,
|
487
|
-
validate_schema: bool = False,
|
488
|
-
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
489
|
-
generation_config: GenerationConfig | NotSet = NOT_SET,
|
490
|
-
output_config: OutputConfig | NotSet = NOT_SET,
|
491
|
-
code_sample_style: str = CodeSampleStyle.default().name,
|
492
|
-
rate_limit: str | None = None,
|
493
|
-
sanitize_output: bool = True,
|
494
|
-
) -> LazySchema:
|
495
|
-
"""Load schema from a ``pytest`` fixture.
|
496
|
-
|
497
|
-
It is useful if you don't want to make network requests during module loading. With this loader you can defer it
|
498
|
-
to a fixture.
|
499
|
-
|
500
|
-
Note, the fixture should return a ``BaseSchema`` instance loaded with another loader.
|
501
|
-
|
502
|
-
:param str fixture_name: The name of a fixture to load.
|
503
|
-
"""
|
504
|
-
from ...lazy import LazySchema
|
505
|
-
|
506
|
-
_code_sample_style = CodeSampleStyle.from_str(code_sample_style)
|
507
|
-
_data_generation_methods: DataGenerationMethodInput | NotSet
|
508
|
-
if data_generation_methods is not NOT_SET:
|
509
|
-
data_generation_methods = cast(DataGenerationMethodInput, data_generation_methods)
|
510
|
-
_data_generation_methods = DataGenerationMethod.ensure_list(data_generation_methods)
|
511
|
-
else:
|
512
|
-
_data_generation_methods = data_generation_methods
|
513
|
-
rate_limiter: Limiter | None = None
|
514
|
-
if rate_limit is not None:
|
515
|
-
rate_limiter = build_limiter(rate_limit)
|
516
|
-
for name in ("method", "endpoint", "tag", "operation_id", "skip_deprecated_operations"):
|
517
|
-
value = locals()[name]
|
518
|
-
if value is not None:
|
519
|
-
warn_filtration_arguments(name)
|
520
|
-
filter_set = filter_set_from_components(
|
521
|
-
include=True,
|
522
|
-
method=method,
|
523
|
-
endpoint=endpoint,
|
524
|
-
tag=tag,
|
525
|
-
operation_id=operation_id,
|
526
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
527
|
-
)
|
528
|
-
return LazySchema(
|
529
|
-
fixture_name,
|
530
|
-
app=app,
|
531
|
-
base_url=base_url,
|
532
|
-
filter_set=filter_set,
|
533
|
-
validate_schema=validate_schema,
|
534
|
-
data_generation_methods=_data_generation_methods,
|
535
|
-
generation_config=generation_config,
|
536
|
-
output_config=output_config,
|
537
|
-
code_sample_style=_code_sample_style,
|
538
|
-
rate_limiter=rate_limiter,
|
539
|
-
sanitize_output=sanitize_output,
|
540
|
-
)
|
541
|
-
|
542
|
-
|
543
|
-
def from_wsgi(
|
544
|
-
schema_path: str,
|
545
|
-
app: Any,
|
546
|
-
*,
|
547
|
-
base_url: str | None = None,
|
548
|
-
method: Filter | None = None,
|
549
|
-
endpoint: Filter | None = None,
|
550
|
-
tag: Filter | None = None,
|
551
|
-
operation_id: Filter | None = None,
|
552
|
-
skip_deprecated_operations: bool | None = None,
|
553
|
-
validate_schema: bool = False,
|
554
|
-
force_schema_version: str | None = None,
|
555
|
-
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
556
|
-
generation_config: GenerationConfig | None = None,
|
557
|
-
output_config: OutputConfig | None = None,
|
558
|
-
code_sample_style: str = CodeSampleStyle.default().name,
|
559
|
-
rate_limit: str | None = None,
|
560
|
-
sanitize_output: bool = True,
|
561
|
-
**kwargs: Any,
|
562
|
-
) -> BaseOpenAPISchema:
|
563
|
-
"""Load Open API schema from a WSGI app.
|
564
|
-
|
565
|
-
:param str schema_path: An in-app relative URL to the schema.
|
566
|
-
:param app: A WSGI app instance.
|
567
|
-
"""
|
568
|
-
from werkzeug.test import Client
|
569
|
-
|
570
|
-
from ...transports.responses import WSGIResponse
|
571
|
-
|
572
|
-
require_relative_url(schema_path)
|
573
|
-
setup_default_headers(kwargs)
|
574
|
-
client = Client(app, WSGIResponse)
|
575
|
-
response = load_schema_from_url(lambda: client.get(schema_path, **kwargs))
|
576
|
-
return from_file(
|
577
|
-
response.data,
|
578
|
-
app=app,
|
579
|
-
base_url=base_url,
|
580
|
-
method=method,
|
581
|
-
endpoint=endpoint,
|
582
|
-
tag=tag,
|
583
|
-
operation_id=operation_id,
|
584
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
585
|
-
validate_schema=validate_schema,
|
586
|
-
force_schema_version=force_schema_version,
|
587
|
-
data_generation_methods=data_generation_methods,
|
588
|
-
generation_config=generation_config,
|
589
|
-
output_config=output_config,
|
590
|
-
code_sample_style=code_sample_style,
|
591
|
-
location=schema_path,
|
592
|
-
rate_limit=rate_limit,
|
593
|
-
sanitize_output=sanitize_output,
|
594
|
-
__expects_json=_is_json_response(response),
|
595
|
-
)
|
596
|
-
|
597
|
-
|
598
|
-
def get_loader_for_app(app: Any) -> Callable:
|
599
|
-
from ...transports.asgi import is_asgi_app
|
600
|
-
|
601
|
-
if is_asgi_app(app):
|
602
|
-
return from_asgi
|
603
|
-
if app.__class__.__module__.startswith("aiohttp."):
|
604
|
-
return from_aiohttp
|
605
|
-
return from_wsgi
|
606
|
-
|
607
|
-
|
608
|
-
def from_aiohttp(
|
609
|
-
schema_path: str,
|
610
|
-
app: Any,
|
611
|
-
*,
|
612
|
-
base_url: str | None = None,
|
613
|
-
method: Filter | None = None,
|
614
|
-
endpoint: Filter | None = None,
|
615
|
-
tag: Filter | None = None,
|
616
|
-
operation_id: Filter | None = None,
|
617
|
-
skip_deprecated_operations: bool | None = None,
|
618
|
-
validate_schema: bool = False,
|
619
|
-
force_schema_version: str | None = None,
|
620
|
-
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
621
|
-
generation_config: GenerationConfig | None = None,
|
622
|
-
output_config: OutputConfig | None = None,
|
623
|
-
code_sample_style: str = CodeSampleStyle.default().name,
|
624
|
-
rate_limit: str | None = None,
|
625
|
-
sanitize_output: bool = True,
|
626
|
-
**kwargs: Any,
|
627
|
-
) -> BaseOpenAPISchema:
|
628
|
-
"""Load Open API schema from an AioHTTP app.
|
629
|
-
|
630
|
-
:param str schema_path: An in-app relative URL to the schema.
|
631
|
-
:param app: An AioHTTP app instance.
|
632
|
-
"""
|
633
|
-
from ...extra._aiohttp import run_server
|
634
|
-
|
635
|
-
port = run_server(app)
|
636
|
-
app_url = f"http://127.0.0.1:{port}/"
|
637
|
-
url = urljoin(app_url, schema_path)
|
638
|
-
return from_uri(
|
639
|
-
url,
|
640
|
-
base_url=base_url,
|
641
|
-
method=method,
|
642
|
-
endpoint=endpoint,
|
643
|
-
tag=tag,
|
644
|
-
operation_id=operation_id,
|
645
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
646
|
-
validate_schema=validate_schema,
|
647
|
-
force_schema_version=force_schema_version,
|
648
|
-
data_generation_methods=data_generation_methods,
|
649
|
-
generation_config=generation_config,
|
650
|
-
output_config=output_config,
|
651
|
-
code_sample_style=code_sample_style,
|
652
|
-
rate_limit=rate_limit,
|
653
|
-
sanitize_output=sanitize_output,
|
654
|
-
**kwargs,
|
655
|
-
)
|
656
|
-
|
657
|
-
|
658
|
-
def from_asgi(
|
659
|
-
schema_path: str,
|
660
|
-
app: Any,
|
661
|
-
*,
|
662
|
-
base_url: str | None = None,
|
663
|
-
method: Filter | None = None,
|
664
|
-
endpoint: Filter | None = None,
|
665
|
-
tag: Filter | None = None,
|
666
|
-
operation_id: Filter | None = None,
|
667
|
-
skip_deprecated_operations: bool | None = None,
|
668
|
-
validate_schema: bool = False,
|
669
|
-
force_schema_version: str | None = None,
|
670
|
-
data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
|
671
|
-
generation_config: GenerationConfig | None = None,
|
672
|
-
output_config: OutputConfig | None = None,
|
673
|
-
code_sample_style: str = CodeSampleStyle.default().name,
|
674
|
-
rate_limit: str | None = None,
|
675
|
-
sanitize_output: bool = True,
|
676
|
-
**kwargs: Any,
|
677
|
-
) -> BaseOpenAPISchema:
|
678
|
-
"""Load Open API schema from an ASGI app.
|
679
|
-
|
680
|
-
:param str schema_path: An in-app relative URL to the schema.
|
681
|
-
:param app: An ASGI app instance.
|
682
|
-
"""
|
683
|
-
from starlette_testclient import TestClient as ASGIClient
|
684
|
-
|
685
|
-
require_relative_url(schema_path)
|
686
|
-
setup_default_headers(kwargs)
|
687
|
-
client = ASGIClient(app)
|
688
|
-
response = load_schema_from_url(lambda: client.get(schema_path, **kwargs))
|
689
|
-
return from_file(
|
690
|
-
response.text,
|
691
|
-
app=app,
|
692
|
-
base_url=base_url,
|
693
|
-
method=method,
|
694
|
-
endpoint=endpoint,
|
695
|
-
tag=tag,
|
696
|
-
operation_id=operation_id,
|
697
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
698
|
-
validate_schema=validate_schema,
|
699
|
-
force_schema_version=force_schema_version,
|
700
|
-
data_generation_methods=data_generation_methods,
|
701
|
-
generation_config=generation_config,
|
702
|
-
output_config=output_config,
|
703
|
-
code_sample_style=code_sample_style,
|
704
|
-
location=schema_path,
|
705
|
-
rate_limit=rate_limit,
|
706
|
-
sanitize_output=sanitize_output,
|
707
|
-
__expects_json=_is_json_response(response),
|
708
|
-
)
|