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
@@ -0,0 +1,55 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections import defaultdict
|
4
|
+
from typing import Any, Callable
|
5
|
+
|
6
|
+
import click
|
7
|
+
|
8
|
+
GROUPS: list[str] = []
|
9
|
+
|
10
|
+
|
11
|
+
class CommandWithGroupedOptions(click.Command):
|
12
|
+
def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
13
|
+
groups = defaultdict(list)
|
14
|
+
for param in self.get_params(ctx):
|
15
|
+
rv = param.get_help_record(ctx)
|
16
|
+
if rv is not None:
|
17
|
+
(option_repr, message) = rv
|
18
|
+
if isinstance(param.type, click.Choice):
|
19
|
+
message += (
|
20
|
+
getattr(param.type, "choices_repr", None)
|
21
|
+
or f" [possible values: {', '.join(param.type.choices)}]"
|
22
|
+
)
|
23
|
+
|
24
|
+
if isinstance(param, GroupedOption):
|
25
|
+
group = param.group
|
26
|
+
else:
|
27
|
+
group = "Global options"
|
28
|
+
groups[group].append((option_repr, message))
|
29
|
+
for group in GROUPS:
|
30
|
+
with formatter.section(group or "Options"):
|
31
|
+
formatter.write_dl(groups[group], col_max=40)
|
32
|
+
|
33
|
+
|
34
|
+
class GroupedOption(click.Option):
|
35
|
+
def __init__(self, *args: Any, group: str | None = None, **kwargs: Any):
|
36
|
+
super().__init__(*args, **kwargs)
|
37
|
+
self.group = group
|
38
|
+
|
39
|
+
|
40
|
+
def group(name: str) -> Callable:
|
41
|
+
GROUPS.append(name)
|
42
|
+
|
43
|
+
def _inner(cmd: Callable) -> Callable:
|
44
|
+
for param in reversed(cmd.__click_params__): # type: ignore[attr-defined]
|
45
|
+
if not isinstance(param, GroupedOption) or param.group is not None:
|
46
|
+
break
|
47
|
+
param.group = name
|
48
|
+
return cmd
|
49
|
+
|
50
|
+
return _inner
|
51
|
+
|
52
|
+
|
53
|
+
def grouped_option(*args: Any, **kwargs: Any) -> Callable:
|
54
|
+
kwargs.setdefault("cls", GroupedOption)
|
55
|
+
return click.option(*args, **kwargs)
|
@@ -1,15 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
3
|
+
from enum import Enum
|
4
|
+
from typing import Any, NoReturn
|
4
5
|
|
5
6
|
import click
|
6
7
|
|
7
|
-
from
|
8
|
-
|
9
|
-
if TYPE_CHECKING:
|
10
|
-
from enum import Enum
|
11
|
-
|
12
|
-
from ..types import NotSet
|
8
|
+
from schemathesis.core import NOT_SET, NotSet
|
9
|
+
from schemathesis.core.registries import Registry
|
13
10
|
|
14
11
|
|
15
12
|
class CustomHelpMessageChoice(click.Choice):
|
@@ -29,13 +26,23 @@ class BaseCsvChoice(click.Choice):
|
|
29
26
|
invalid_options = set(selected) - set(self.choices)
|
30
27
|
return selected, invalid_options
|
31
28
|
|
32
|
-
def fail_on_invalid_options(self, invalid_options: set[str], selected: list[str]) -> NoReturn:
|
29
|
+
def fail_on_invalid_options(self, invalid_options: set[str], selected: list[str]) -> NoReturn: # type: ignore[misc]
|
33
30
|
# Sort to keep the error output consistent with the passed values
|
34
31
|
sorted_options = ", ".join(sorted(invalid_options, key=selected.index))
|
35
32
|
available_options = ", ".join(self.choices)
|
36
33
|
self.fail(f"invalid choice(s): {sorted_options}. Choose from {available_options}.")
|
37
34
|
|
38
35
|
|
36
|
+
class CsvChoice(BaseCsvChoice):
|
37
|
+
def convert( # type: ignore[return]
|
38
|
+
self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
|
39
|
+
) -> list[str]:
|
40
|
+
selected, invalid_options = self.parse_value(value)
|
41
|
+
if not invalid_options and selected:
|
42
|
+
return selected
|
43
|
+
self.fail_on_invalid_options(invalid_options, selected)
|
44
|
+
|
45
|
+
|
39
46
|
class CsvEnumChoice(BaseCsvChoice):
|
40
47
|
def __init__(self, choices: type[Enum]):
|
41
48
|
self.enum = choices
|
@@ -50,14 +57,6 @@ class CsvEnumChoice(BaseCsvChoice):
|
|
50
57
|
self.fail_on_invalid_options(invalid_options, selected)
|
51
58
|
|
52
59
|
|
53
|
-
class CsvChoice(BaseCsvChoice):
|
54
|
-
def convert(self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None) -> list[str]:
|
55
|
-
selected, invalid_options = self.parse_value(value)
|
56
|
-
if not invalid_options and selected:
|
57
|
-
return selected
|
58
|
-
self.fail_on_invalid_options(invalid_options, selected)
|
59
|
-
|
60
|
-
|
61
60
|
class CsvListChoice(click.ParamType):
|
62
61
|
def convert( # type: ignore[return]
|
63
62
|
self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
|
@@ -65,6 +64,28 @@ class CsvListChoice(click.ParamType):
|
|
65
64
|
return [item for item in value.split(",") if item]
|
66
65
|
|
67
66
|
|
67
|
+
class RegistryChoice(BaseCsvChoice):
|
68
|
+
def __init__(self, registry: Registry, with_all: bool = False) -> None:
|
69
|
+
self.registry = registry
|
70
|
+
self.case_sensitive = True
|
71
|
+
self.with_all = with_all
|
72
|
+
|
73
|
+
@property
|
74
|
+
def choices(self) -> list[str]:
|
75
|
+
choices = self.registry.get_all_names()
|
76
|
+
if self.with_all:
|
77
|
+
choices.append("all")
|
78
|
+
return choices
|
79
|
+
|
80
|
+
def convert( # type: ignore[return]
|
81
|
+
self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
|
82
|
+
) -> list[str]:
|
83
|
+
selected, invalid_options = self.parse_value(value)
|
84
|
+
if not invalid_options and selected:
|
85
|
+
return selected
|
86
|
+
self.fail_on_invalid_options(invalid_options, selected)
|
87
|
+
|
88
|
+
|
68
89
|
class OptionalInt(click.types.IntRange):
|
69
90
|
def convert( # type: ignore
|
70
91
|
self, value: str, param: click.core.Parameter | None, ctx: click.core.Context | None
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
|
4
|
+
import click
|
5
|
+
|
6
|
+
from schemathesis.cli.constants import EXTENSIONS_DOCUMENTATION_URL
|
7
|
+
from schemathesis.core.errors import format_exception
|
8
|
+
|
9
|
+
HOOKS_MODULE_ENV_VAR = "SCHEMATHESIS_HOOKS"
|
10
|
+
|
11
|
+
|
12
|
+
def load() -> None:
|
13
|
+
hooks = os.getenv(HOOKS_MODULE_ENV_VAR)
|
14
|
+
if hooks:
|
15
|
+
_load(hooks)
|
16
|
+
|
17
|
+
|
18
|
+
def _load(module_name: str) -> None:
|
19
|
+
"""Load the given hook by importing it."""
|
20
|
+
try:
|
21
|
+
sys.path.append(os.getcwd()) # fix ModuleNotFoundError module in cwd
|
22
|
+
__import__(module_name)
|
23
|
+
except Exception as exc:
|
24
|
+
click.secho("Unable to load Schemathesis extension hooks", fg="red", bold=True)
|
25
|
+
formatted_module_name = click.style(f"'{module_name}'", bold=True)
|
26
|
+
if isinstance(exc, ModuleNotFoundError) and exc.name == module_name:
|
27
|
+
click.echo(
|
28
|
+
f"\nAn attempt to import the module {formatted_module_name} failed because it could not be found."
|
29
|
+
)
|
30
|
+
click.echo("\nEnsure the module name is correctly spelled and reachable from the current directory.")
|
31
|
+
else:
|
32
|
+
click.echo(f"\nAn error occurred while importing the module {formatted_module_name}. Traceback:")
|
33
|
+
message = format_exception(exc, with_traceback=True, skip_frames=1)
|
34
|
+
click.secho(f"\n{message}", fg="red")
|
35
|
+
click.echo(f"\nFor more information on how to work with hooks, visit {EXTENSIONS_DOCUMENTATION_URL}")
|
36
|
+
raise click.exceptions.Exit(1) from None
|
schemathesis/contrib/__init__.py
CHANGED
@@ -1,11 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from
|
4
|
-
|
5
|
-
from ...hooks import HookContext, register, unregister
|
6
|
-
|
7
|
-
if TYPE_CHECKING:
|
8
|
-
from ...models import Case
|
3
|
+
from schemathesis.generation.case import Case
|
4
|
+
from schemathesis.hooks import HookContext, register, unregister
|
9
5
|
|
10
6
|
|
11
7
|
def install() -> None:
|
@@ -18,7 +14,7 @@ def uninstall() -> None:
|
|
18
14
|
|
19
15
|
def before_add_examples(context: HookContext, examples: list[Case]) -> None:
|
20
16
|
if not examples and context.operation is not None:
|
21
|
-
from
|
17
|
+
from schemathesis.generation.hypothesis.examples import add_single_example
|
22
18
|
|
23
19
|
strategy = context.operation.as_strategy()
|
24
20
|
add_single_example(strategy, examples)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import enum
|
4
|
+
from dataclasses import dataclass
|
5
|
+
|
6
|
+
SCHEMATHESIS_TEST_CASE_HEADER = "X-Schemathesis-TestCaseId"
|
7
|
+
|
8
|
+
|
9
|
+
class NotSet: ...
|
10
|
+
|
11
|
+
|
12
|
+
NOT_SET = NotSet()
|
13
|
+
|
14
|
+
|
15
|
+
class SpecificationFeature(str, enum.Enum):
|
16
|
+
"""Features that Schemathesis can provide for different specifications."""
|
17
|
+
|
18
|
+
STATEFUL_TESTING = "stateful_testing"
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class Specification:
|
23
|
+
kind: SpecificationKind
|
24
|
+
version: str
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
def openapi(cls, version: str) -> Specification:
|
28
|
+
return cls(kind=SpecificationKind.OPENAPI, version=version)
|
29
|
+
|
30
|
+
@classmethod
|
31
|
+
def graphql(cls, version: str) -> Specification:
|
32
|
+
return cls(kind=SpecificationKind.GRAPHQL, version=version)
|
33
|
+
|
34
|
+
@property
|
35
|
+
def name(self) -> str:
|
36
|
+
name = {SpecificationKind.GRAPHQL: "GraphQL", SpecificationKind.OPENAPI: "Open API"}[self.kind]
|
37
|
+
return f"{name} {self.version}".strip()
|
38
|
+
|
39
|
+
def supports_feature(self, feature: SpecificationFeature) -> bool:
|
40
|
+
"""Check if Schemathesis supports a given feature for this specification."""
|
41
|
+
if self.kind == SpecificationKind.OPENAPI:
|
42
|
+
return feature in {SpecificationFeature.STATEFUL_TESTING}
|
43
|
+
return False
|
44
|
+
|
45
|
+
|
46
|
+
class SpecificationKind(str, enum.Enum):
|
47
|
+
"""Specification of the given schema."""
|
48
|
+
|
49
|
+
OPENAPI = "openapi"
|
50
|
+
GRAPHQL = "graphql"
|
51
|
+
|
52
|
+
|
53
|
+
def string_to_boolean(value: str) -> str | bool:
|
54
|
+
if value.lower() in ("y", "yes", "t", "true", "on", "1"):
|
55
|
+
return True
|
56
|
+
if value.lower() in ("n", "no", "f", "false", "off", "0"):
|
57
|
+
return False
|
58
|
+
return value
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from jsonschema import RefResolutionError
|
7
|
+
|
8
|
+
try:
|
9
|
+
BaseExceptionGroup = BaseExceptionGroup # type: ignore
|
10
|
+
except NameError:
|
11
|
+
from exceptiongroup import BaseExceptionGroup # type: ignore
|
12
|
+
|
13
|
+
|
14
|
+
def __getattr__(name: str) -> type[RefResolutionError] | type[BaseExceptionGroup]:
|
15
|
+
if name == "RefResolutionError":
|
16
|
+
# Import it just once to keep just a single warning
|
17
|
+
from jsonschema import RefResolutionError
|
18
|
+
|
19
|
+
return RefResolutionError
|
20
|
+
if name == "BaseExceptionGroup":
|
21
|
+
return BaseExceptionGroup
|
22
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
23
|
+
|
24
|
+
|
25
|
+
__all__ = ["BaseExceptionGroup", "RefResolutionError"]
|
@@ -0,0 +1,58 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from functools import lru_cache
|
4
|
+
from shlex import quote
|
5
|
+
from typing import TYPE_CHECKING, Any
|
6
|
+
|
7
|
+
from schemathesis.core import SCHEMATHESIS_TEST_CASE_HEADER
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from requests.models import CaseInsensitiveDict
|
11
|
+
|
12
|
+
|
13
|
+
def generate(
|
14
|
+
*,
|
15
|
+
method: str,
|
16
|
+
url: str,
|
17
|
+
body: str | bytes | None,
|
18
|
+
verify: bool,
|
19
|
+
headers: dict[str, Any],
|
20
|
+
known_generated_headers: dict[str, Any] | None,
|
21
|
+
) -> str:
|
22
|
+
"""Generate a code snippet for making HTTP requests."""
|
23
|
+
_filter_headers(headers, known_generated_headers or {})
|
24
|
+
command = f"curl -X {method}"
|
25
|
+
for key, value in headers.items():
|
26
|
+
header = f"{key}: {value}"
|
27
|
+
command += f" -H {quote(header)}"
|
28
|
+
if body:
|
29
|
+
if isinstance(body, bytes):
|
30
|
+
body = body.decode("utf-8", errors="replace")
|
31
|
+
command += f" -d {quote(body)}"
|
32
|
+
if not verify:
|
33
|
+
command += " --insecure"
|
34
|
+
return f"{command} {quote(url)}"
|
35
|
+
|
36
|
+
|
37
|
+
def _filter_headers(headers: dict[str, Any], known_generated_headers: dict[str, Any]) -> None:
|
38
|
+
for key in list(headers):
|
39
|
+
if key not in known_generated_headers and key in get_excluded_headers():
|
40
|
+
del headers[key]
|
41
|
+
|
42
|
+
|
43
|
+
@lru_cache
|
44
|
+
def get_excluded_headers() -> CaseInsensitiveDict:
|
45
|
+
from requests.structures import CaseInsensitiveDict
|
46
|
+
from requests.utils import default_headers
|
47
|
+
|
48
|
+
# These headers are added automatically by Schemathesis or `requests`.
|
49
|
+
# Do not show them in code samples to make them more readable
|
50
|
+
|
51
|
+
return CaseInsensitiveDict(
|
52
|
+
{
|
53
|
+
"Content-Length": None,
|
54
|
+
"Transfer-Encoding": None,
|
55
|
+
SCHEMATHESIS_TEST_CASE_HEADER: None,
|
56
|
+
**default_headers(),
|
57
|
+
}
|
58
|
+
)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
from functools import lru_cache
|
5
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, TextIO
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
import yaml
|
9
|
+
|
10
|
+
|
11
|
+
@lru_cache
|
12
|
+
def get_yaml_loader() -> type[yaml.SafeLoader]:
|
13
|
+
"""Create a YAML loader, that doesn't parse specific tokens into Python objects."""
|
14
|
+
import yaml
|
15
|
+
|
16
|
+
try:
|
17
|
+
from yaml import CSafeLoader as SafeLoader
|
18
|
+
except ImportError:
|
19
|
+
from yaml import SafeLoader # type: ignore
|
20
|
+
|
21
|
+
cls: type[yaml.SafeLoader] = type("YAMLLoader", (SafeLoader,), {})
|
22
|
+
cls.yaml_implicit_resolvers = {
|
23
|
+
key: [(tag, regexp) for tag, regexp in mapping if tag != "tag:yaml.org,2002:timestamp"]
|
24
|
+
for key, mapping in cls.yaml_implicit_resolvers.copy().items()
|
25
|
+
}
|
26
|
+
|
27
|
+
# Fix pyyaml scientific notation parse bug
|
28
|
+
# See PR: https://github.com/yaml/pyyaml/pull/174 for upstream fix
|
29
|
+
cls.add_implicit_resolver( # type: ignore
|
30
|
+
"tag:yaml.org,2002:float",
|
31
|
+
re.compile(
|
32
|
+
r"""^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+]?[0-9]+)?
|
33
|
+
|[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+)
|
34
|
+
|\.[0-9_]+(?:[eE][-+]?[0-9]+)?
|
35
|
+
|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*
|
36
|
+
|[-+]?\.(?:inf|Inf|INF)
|
37
|
+
|\.(?:nan|NaN|NAN))$""",
|
38
|
+
re.VERBOSE,
|
39
|
+
),
|
40
|
+
list("-+0123456789."),
|
41
|
+
)
|
42
|
+
|
43
|
+
def construct_mapping(self: SafeLoader, node: yaml.Node, deep: bool = False) -> dict[str, Any]:
|
44
|
+
if isinstance(node, yaml.MappingNode):
|
45
|
+
self.flatten_mapping(node) # type: ignore
|
46
|
+
mapping = {}
|
47
|
+
for key_node, value_node in node.value:
|
48
|
+
# If the key has a tag different from `str` - use its string value.
|
49
|
+
# With this change all integer keys or YAML 1.1 boolean-ish values like "on" / "off" will not be cast to
|
50
|
+
# a different type
|
51
|
+
if key_node.tag != "tag:yaml.org,2002:str":
|
52
|
+
key = key_node.value
|
53
|
+
else:
|
54
|
+
key = self.construct_object(key_node, deep) # type: ignore
|
55
|
+
mapping[key] = self.construct_object(value_node, deep) # type: ignore
|
56
|
+
return mapping
|
57
|
+
|
58
|
+
cls.construct_mapping = construct_mapping # type: ignore
|
59
|
+
return cls
|
60
|
+
|
61
|
+
|
62
|
+
def deserialize_yaml(stream: str | bytes | TextIO | BinaryIO) -> Any:
|
63
|
+
import yaml
|
64
|
+
|
65
|
+
return yaml.load(stream, get_yaml_loader())
|