schemathesis 3.21.2__py3-none-any.whl → 3.22.1__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 +1 -1
- schemathesis/_compat.py +2 -18
- schemathesis/_dependency_versions.py +1 -6
- schemathesis/_hypothesis.py +15 -12
- schemathesis/_lazy_import.py +3 -2
- schemathesis/_xml.py +12 -11
- schemathesis/auths.py +88 -81
- schemathesis/checks.py +4 -4
- schemathesis/cli/__init__.py +202 -171
- schemathesis/cli/callbacks.py +29 -32
- schemathesis/cli/cassettes.py +25 -25
- schemathesis/cli/context.py +18 -12
- schemathesis/cli/junitxml.py +2 -2
- schemathesis/cli/options.py +10 -11
- schemathesis/cli/output/default.py +64 -34
- schemathesis/code_samples.py +10 -10
- schemathesis/constants.py +1 -1
- schemathesis/contrib/unique_data.py +2 -2
- schemathesis/exceptions.py +55 -42
- schemathesis/extra/_aiohttp.py +2 -2
- schemathesis/extra/_flask.py +2 -2
- schemathesis/extra/_server.py +3 -2
- schemathesis/extra/pytest_plugin.py +10 -10
- schemathesis/failures.py +16 -16
- schemathesis/filters.py +40 -41
- schemathesis/fixups/__init__.py +4 -3
- schemathesis/fixups/fast_api.py +5 -4
- schemathesis/generation/__init__.py +16 -4
- schemathesis/hooks.py +25 -25
- schemathesis/internal/jsonschema.py +4 -3
- schemathesis/internal/transformation.py +3 -2
- schemathesis/lazy.py +39 -31
- schemathesis/loaders.py +8 -8
- schemathesis/models.py +128 -126
- schemathesis/parameters.py +6 -5
- schemathesis/runner/__init__.py +107 -81
- schemathesis/runner/events.py +37 -26
- schemathesis/runner/impl/core.py +86 -81
- schemathesis/runner/impl/solo.py +19 -15
- schemathesis/runner/impl/threadpool.py +40 -22
- schemathesis/runner/serialization.py +67 -40
- schemathesis/sanitization.py +18 -20
- schemathesis/schemas.py +83 -72
- schemathesis/serializers.py +39 -30
- schemathesis/service/ci.py +20 -21
- schemathesis/service/client.py +29 -9
- schemathesis/service/constants.py +1 -0
- schemathesis/service/events.py +2 -2
- schemathesis/service/hosts.py +8 -7
- schemathesis/service/metadata.py +5 -0
- schemathesis/service/models.py +22 -4
- schemathesis/service/report.py +15 -15
- schemathesis/service/serialization.py +23 -27
- schemathesis/service/usage.py +8 -7
- schemathesis/specs/graphql/loaders.py +31 -24
- schemathesis/specs/graphql/nodes.py +3 -2
- schemathesis/specs/graphql/scalars.py +26 -2
- schemathesis/specs/graphql/schemas.py +38 -34
- schemathesis/specs/openapi/_hypothesis.py +62 -44
- schemathesis/specs/openapi/checks.py +10 -10
- schemathesis/specs/openapi/converter.py +10 -9
- schemathesis/specs/openapi/definitions.py +2 -2
- schemathesis/specs/openapi/examples.py +22 -21
- schemathesis/specs/openapi/expressions/nodes.py +5 -4
- schemathesis/specs/openapi/expressions/parser.py +7 -6
- schemathesis/specs/openapi/filters.py +6 -6
- schemathesis/specs/openapi/formats.py +2 -2
- schemathesis/specs/openapi/links.py +19 -21
- schemathesis/specs/openapi/loaders.py +133 -78
- schemathesis/specs/openapi/negative/__init__.py +16 -11
- schemathesis/specs/openapi/negative/mutations.py +11 -10
- schemathesis/specs/openapi/parameters.py +20 -19
- schemathesis/specs/openapi/references.py +21 -20
- schemathesis/specs/openapi/schemas.py +97 -84
- schemathesis/specs/openapi/security.py +25 -24
- schemathesis/specs/openapi/serialization.py +20 -23
- schemathesis/specs/openapi/stateful/__init__.py +12 -11
- schemathesis/specs/openapi/stateful/links.py +7 -7
- schemathesis/specs/openapi/utils.py +4 -3
- schemathesis/specs/openapi/validation.py +3 -2
- schemathesis/stateful/__init__.py +15 -16
- schemathesis/stateful/state_machine.py +9 -9
- schemathesis/targets.py +3 -3
- schemathesis/throttling.py +2 -2
- schemathesis/transports/auth.py +2 -2
- schemathesis/transports/content_types.py +5 -0
- schemathesis/transports/headers.py +3 -2
- schemathesis/transports/responses.py +1 -1
- schemathesis/utils.py +7 -10
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
- schemathesis-3.22.1.dist-info/RECORD +130 -0
- schemathesis-3.21.2.dist-info/RECORD +0 -130
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/cli/__init__.py
CHANGED
|
@@ -7,17 +7,16 @@ import sys
|
|
|
7
7
|
import traceback
|
|
8
8
|
import warnings
|
|
9
9
|
from collections import defaultdict
|
|
10
|
-
from contextlib import suppress
|
|
11
10
|
from dataclasses import dataclass
|
|
12
11
|
from enum import Enum
|
|
13
12
|
from queue import Queue
|
|
14
|
-
from typing import Any, Callable,
|
|
13
|
+
from typing import Any, Callable, Generator, Iterable, NoReturn, cast, TYPE_CHECKING
|
|
15
14
|
from urllib.parse import urlparse
|
|
16
15
|
|
|
17
16
|
import click
|
|
18
17
|
|
|
19
18
|
from .. import checks as checks_module
|
|
20
|
-
from .. import contrib, experimental
|
|
19
|
+
from .. import contrib, experimental, generation
|
|
21
20
|
from .. import fixups as _fixups
|
|
22
21
|
from .. import runner, service
|
|
23
22
|
from .. import targets as targets_module
|
|
@@ -35,7 +34,7 @@ from ..constants import (
|
|
|
35
34
|
EXTENSIONS_DOCUMENTATION_URL,
|
|
36
35
|
ISSUE_TRACKER_URL,
|
|
37
36
|
)
|
|
38
|
-
from ..exceptions import SchemaError, extract_nth_traceback
|
|
37
|
+
from ..exceptions import SchemaError, extract_nth_traceback, SchemaErrorType
|
|
39
38
|
from ..fixups import ALL_FIXUPS
|
|
40
39
|
from ..loaders import load_app, load_yaml
|
|
41
40
|
from ..transports.auth import get_requests_auth
|
|
@@ -66,7 +65,7 @@ if TYPE_CHECKING:
|
|
|
66
65
|
from .handlers import EventHandler
|
|
67
66
|
|
|
68
67
|
|
|
69
|
-
def _get_callable_names(items:
|
|
68
|
+
def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
|
|
70
69
|
return tuple(item.__name__ for item in items)
|
|
71
70
|
|
|
72
71
|
|
|
@@ -91,6 +90,10 @@ DEPRECATED_PRE_RUN_OPTION_WARNING = (
|
|
|
91
90
|
"Warning: Option `--pre-run` is deprecated and will be removed in Schemathesis 4.0. "
|
|
92
91
|
f"Use the `{HOOKS_MODULE_ENV_VAR}` environment variable instead"
|
|
93
92
|
)
|
|
93
|
+
DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING = (
|
|
94
|
+
"Warning: Option `--show-errors-tracebacks` is deprecated and will be removed in Schemathesis 4.0. "
|
|
95
|
+
"Use `--show-trace` instead"
|
|
96
|
+
)
|
|
94
97
|
CASSETTES_PATH_INVALID_USAGE_MESSAGE = "Can't use `--store-network-log` and `--cassette-path` simultaneously"
|
|
95
98
|
COLOR_OPTIONS_INVALID_USAGE_MESSAGE = "Can't use `--no-color` and `--force-color` simultaneously"
|
|
96
99
|
|
|
@@ -109,29 +112,13 @@ def reset_targets() -> None:
|
|
|
109
112
|
TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS) + ("all",)
|
|
110
113
|
|
|
111
114
|
|
|
112
|
-
class DeprecatedOption(click.Option):
|
|
113
|
-
def __init__(self, *args: Any, removed_in: str, **kwargs: Any) -> None:
|
|
114
|
-
super().__init__(*args, **kwargs)
|
|
115
|
-
self.removed_in = removed_in
|
|
116
|
-
|
|
117
|
-
def handle_parse_result(self, ctx: click.Context, opts: Dict[str, Any], args: List[str]) -> Tuple[Any, List[str]]:
|
|
118
|
-
if self.name in opts:
|
|
119
|
-
opt_names = "/".join(f"`{name}`" for name in self.opts)
|
|
120
|
-
verb = "is" if len(self.opts) == 1 else "are"
|
|
121
|
-
click.secho(
|
|
122
|
-
f"\nWARNING: {opt_names} {verb} deprecated and will be removed in Schemathesis {self.removed_in}\n",
|
|
123
|
-
fg="yellow",
|
|
124
|
-
)
|
|
125
|
-
return super().handle_parse_result(ctx, opts, args)
|
|
126
|
-
|
|
127
|
-
|
|
128
115
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
129
116
|
@click.option("--pre-run", help="A module to execute before running the tests.", type=str, hidden=True)
|
|
130
117
|
@click.version_option()
|
|
131
|
-
def schemathesis(pre_run:
|
|
118
|
+
def schemathesis(pre_run: str | None = None) -> None:
|
|
132
119
|
"""Automated API testing employing fuzzing techniques for OpenAPI and GraphQL."""
|
|
133
120
|
# Don't use `envvar=HOOKS_MODULE_ENV_VAR` arg to raise a deprecation warning for hooks
|
|
134
|
-
hooks:
|
|
121
|
+
hooks: str | None
|
|
135
122
|
if pre_run:
|
|
136
123
|
click.secho(DEPRECATED_PRE_RUN_OPTION_WARNING, fg="yellow")
|
|
137
124
|
hooks = pre_run
|
|
@@ -463,6 +450,15 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
463
450
|
is_flag=True,
|
|
464
451
|
is_eager=True,
|
|
465
452
|
default=False,
|
|
453
|
+
hidden=True,
|
|
454
|
+
show_default=True,
|
|
455
|
+
)
|
|
456
|
+
@click.option(
|
|
457
|
+
"--show-trace",
|
|
458
|
+
help="Displays complete traceback information for internal errors.",
|
|
459
|
+
is_flag=True,
|
|
460
|
+
is_eager=True,
|
|
461
|
+
default=False,
|
|
466
462
|
show_default=True,
|
|
467
463
|
)
|
|
468
464
|
@click.option(
|
|
@@ -486,8 +482,9 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
486
482
|
)
|
|
487
483
|
@click.option(
|
|
488
484
|
"--store-network-log",
|
|
489
|
-
help="
|
|
485
|
+
help="Saves the test outcomes in a VCR-compatible format.",
|
|
490
486
|
type=click.File("w", encoding="utf-8"),
|
|
487
|
+
hidden=True,
|
|
491
488
|
)
|
|
492
489
|
@click.option(
|
|
493
490
|
"--fixups",
|
|
@@ -515,8 +512,7 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
515
512
|
default=DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
516
513
|
show_default=True,
|
|
517
514
|
type=click.IntRange(1, 100),
|
|
518
|
-
|
|
519
|
-
removed_in="4.0",
|
|
515
|
+
hidden=True,
|
|
520
516
|
)
|
|
521
517
|
@click.option(
|
|
522
518
|
"--force-schema-version",
|
|
@@ -628,6 +624,20 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
628
624
|
callback=callbacks.convert_experimental,
|
|
629
625
|
multiple=True,
|
|
630
626
|
)
|
|
627
|
+
@click.option(
|
|
628
|
+
"--generation-allow-x00",
|
|
629
|
+
help="Determines whether to allow the generation of `\x00` bytes within strings.",
|
|
630
|
+
type=str,
|
|
631
|
+
default="true",
|
|
632
|
+
show_default=True,
|
|
633
|
+
callback=callbacks.convert_boolean_string,
|
|
634
|
+
)
|
|
635
|
+
@click.option(
|
|
636
|
+
"--generation-codec",
|
|
637
|
+
help="Specifies the codec used for generating strings.",
|
|
638
|
+
type=str,
|
|
639
|
+
default="utf-8",
|
|
640
|
+
)
|
|
631
641
|
@click.option(
|
|
632
642
|
"--schemathesis-io-token",
|
|
633
643
|
help="Schemathesis.io authentication token.",
|
|
@@ -656,61 +666,64 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
|
|
|
656
666
|
def run(
|
|
657
667
|
ctx: click.Context,
|
|
658
668
|
schema: str,
|
|
659
|
-
api_name:
|
|
660
|
-
auth:
|
|
669
|
+
api_name: str | None,
|
|
670
|
+
auth: tuple[str, str] | None,
|
|
661
671
|
auth_type: str,
|
|
662
|
-
headers:
|
|
672
|
+
headers: dict[str, str],
|
|
663
673
|
experimental: list,
|
|
664
674
|
checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
|
|
665
675
|
exclude_checks: Iterable[str] = (),
|
|
666
|
-
data_generation_methods:
|
|
667
|
-
max_response_time:
|
|
676
|
+
data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
|
|
677
|
+
max_response_time: int | None = None,
|
|
668
678
|
targets: Iterable[str] = DEFAULT_TARGETS_NAMES,
|
|
669
679
|
exit_first: bool = False,
|
|
670
|
-
max_failures:
|
|
680
|
+
max_failures: int | None = None,
|
|
671
681
|
dry_run: bool = False,
|
|
672
|
-
endpoints:
|
|
673
|
-
methods:
|
|
674
|
-
tags:
|
|
675
|
-
operation_ids:
|
|
682
|
+
endpoints: Filter | None = None,
|
|
683
|
+
methods: Filter | None = None,
|
|
684
|
+
tags: Filter | None = None,
|
|
685
|
+
operation_ids: Filter | None = None,
|
|
676
686
|
workers_num: int = DEFAULT_WORKERS,
|
|
677
|
-
base_url:
|
|
678
|
-
app:
|
|
679
|
-
request_timeout:
|
|
687
|
+
base_url: str | None = None,
|
|
688
|
+
app: str | None = None,
|
|
689
|
+
request_timeout: int | None = None,
|
|
680
690
|
request_tls_verify: bool = True,
|
|
681
|
-
request_cert:
|
|
682
|
-
request_cert_key:
|
|
691
|
+
request_cert: str | None = None,
|
|
692
|
+
request_cert_key: str | None = None,
|
|
683
693
|
validate_schema: bool = True,
|
|
684
694
|
skip_deprecated_operations: bool = False,
|
|
685
|
-
junit_xml:
|
|
686
|
-
debug_output_file:
|
|
695
|
+
junit_xml: click.utils.LazyFile | None = None,
|
|
696
|
+
debug_output_file: click.utils.LazyFile | None = None,
|
|
687
697
|
show_errors_tracebacks: bool = False,
|
|
698
|
+
show_trace: bool = False,
|
|
688
699
|
code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
|
|
689
|
-
cassette_path:
|
|
700
|
+
cassette_path: click.utils.LazyFile | None = None,
|
|
690
701
|
cassette_preserve_exact_body_bytes: bool = False,
|
|
691
|
-
store_network_log:
|
|
692
|
-
wait_for_schema:
|
|
693
|
-
fixups:
|
|
694
|
-
rate_limit:
|
|
695
|
-
stateful:
|
|
702
|
+
store_network_log: click.utils.LazyFile | None = None,
|
|
703
|
+
wait_for_schema: float | None = None,
|
|
704
|
+
fixups: tuple[str] = (), # type: ignore
|
|
705
|
+
rate_limit: str | None = None,
|
|
706
|
+
stateful: Stateful | None = None,
|
|
696
707
|
stateful_recursion_limit: int = DEFAULT_STATEFUL_RECURSION_LIMIT,
|
|
697
|
-
force_schema_version:
|
|
708
|
+
force_schema_version: str | None = None,
|
|
698
709
|
sanitize_output: bool = True,
|
|
699
710
|
contrib_unique_data: bool = False,
|
|
700
711
|
contrib_openapi_formats_uuid: bool = False,
|
|
701
|
-
hypothesis_database:
|
|
702
|
-
hypothesis_deadline:
|
|
703
|
-
hypothesis_derandomize:
|
|
704
|
-
hypothesis_max_examples:
|
|
705
|
-
hypothesis_phases:
|
|
706
|
-
hypothesis_report_multiple_bugs:
|
|
707
|
-
hypothesis_suppress_health_check:
|
|
708
|
-
hypothesis_seed:
|
|
709
|
-
hypothesis_verbosity:
|
|
712
|
+
hypothesis_database: str | None = None,
|
|
713
|
+
hypothesis_deadline: int | NotSet | None = None,
|
|
714
|
+
hypothesis_derandomize: bool | None = None,
|
|
715
|
+
hypothesis_max_examples: int | None = None,
|
|
716
|
+
hypothesis_phases: list[Phase] | None = None,
|
|
717
|
+
hypothesis_report_multiple_bugs: bool | None = None,
|
|
718
|
+
hypothesis_suppress_health_check: list[HealthCheck] | None = None,
|
|
719
|
+
hypothesis_seed: int | None = None,
|
|
720
|
+
hypothesis_verbosity: hypothesis.Verbosity | None = None,
|
|
710
721
|
verbosity: int = 0,
|
|
711
722
|
no_color: bool = False,
|
|
712
|
-
report_value:
|
|
713
|
-
|
|
723
|
+
report_value: str | None = None,
|
|
724
|
+
generation_allow_x00: bool = True,
|
|
725
|
+
generation_codec: str = "utf-8",
|
|
726
|
+
schemathesis_io_token: str | None = None,
|
|
714
727
|
schemathesis_io_url: str = service.DEFAULT_URL,
|
|
715
728
|
schemathesis_io_telemetry: bool = True,
|
|
716
729
|
hosts_file: PathLike = service.DEFAULT_HOSTS_PATH,
|
|
@@ -722,20 +735,26 @@ def run(
|
|
|
722
735
|
|
|
723
736
|
[Optional] API_NAME: Identifier for uploading test data to Schemathesis.io.
|
|
724
737
|
"""
|
|
725
|
-
_hypothesis_phases:
|
|
738
|
+
_hypothesis_phases: list[hypothesis.Phase] | None = None
|
|
726
739
|
if hypothesis_phases is not None:
|
|
727
740
|
_hypothesis_phases = [phase.as_hypothesis() for phase in hypothesis_phases]
|
|
728
|
-
_hypothesis_suppress_health_check:
|
|
741
|
+
_hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None
|
|
729
742
|
if hypothesis_suppress_health_check is not None:
|
|
730
743
|
_hypothesis_suppress_health_check = [
|
|
731
744
|
health_check.as_hypothesis() for health_check in hypothesis_suppress_health_check
|
|
732
745
|
]
|
|
733
746
|
|
|
747
|
+
if show_errors_tracebacks:
|
|
748
|
+
click.secho(DEPRECATED_SHOW_ERROR_TRACEBACKS_OPTION_WARNING, fg="yellow")
|
|
749
|
+
show_trace = show_errors_tracebacks
|
|
750
|
+
|
|
734
751
|
# Enable selected experiments
|
|
735
752
|
for experiment in experimental:
|
|
736
753
|
experiment.enable()
|
|
737
754
|
|
|
738
|
-
|
|
755
|
+
generation_config = generation.GenerationConfig(allow_x00=generation_allow_x00, codec=generation_codec)
|
|
756
|
+
|
|
757
|
+
report: ReportToService | click.utils.LazyFile | None
|
|
739
758
|
if report_value is None:
|
|
740
759
|
report = None
|
|
741
760
|
elif report_value:
|
|
@@ -762,6 +781,7 @@ def run(
|
|
|
762
781
|
schema_kind = callbacks.parse_schema_kind(schema, app)
|
|
763
782
|
callbacks.validate_schema(schema, schema_kind, base_url=base_url, dry_run=dry_run, app=app, api_name=api_name)
|
|
764
783
|
client = None
|
|
784
|
+
schema_or_location: str | dict[str, Any] = schema
|
|
765
785
|
if schema_kind == callbacks.SchemaInputKind.NAME:
|
|
766
786
|
api_name = schema
|
|
767
787
|
if (
|
|
@@ -785,18 +805,7 @@ def run(
|
|
|
785
805
|
f"\nYou've specified an API name, suggesting you want to upload data to {bold(hostname)}. "
|
|
786
806
|
"However, your CLI is not currently authenticated."
|
|
787
807
|
)
|
|
788
|
-
|
|
789
|
-
click.secho(f"1. Retrieve your token from {bold(hostname)}")
|
|
790
|
-
click.secho(f"2. Execute {bold('`st auth login <TOKEN>`')}")
|
|
791
|
-
env_var = bold(f"`{service.TOKEN_ENV_VAR}`")
|
|
792
|
-
click.secho(
|
|
793
|
-
f"\nAs an alternative, supply the token directly "
|
|
794
|
-
f"using the {bold('`--schemathesis-io-token`')} option "
|
|
795
|
-
f"or the {env_var} environment variable."
|
|
796
|
-
)
|
|
797
|
-
click.echo(
|
|
798
|
-
"\nFor more information, please visit: https://schemathesis.readthedocs.io/en/stable/service.html"
|
|
799
|
-
)
|
|
808
|
+
output.default.display_service_unauthorized(hostname)
|
|
800
809
|
raise click.exceptions.Exit(1) from None
|
|
801
810
|
name: str = cast(str, api_name)
|
|
802
811
|
import requests
|
|
@@ -804,8 +813,9 @@ def run(
|
|
|
804
813
|
try:
|
|
805
814
|
details = client.get_api_details(name)
|
|
806
815
|
# Replace config values with ones loaded from the service
|
|
807
|
-
|
|
808
|
-
|
|
816
|
+
schema_or_location = details.specification.schema
|
|
817
|
+
default_environment = details.default_environment
|
|
818
|
+
base_url = base_url or (default_environment.url if default_environment else None)
|
|
809
819
|
except requests.HTTPError as exc:
|
|
810
820
|
handle_service_error(exc, name)
|
|
811
821
|
if report is REPORT_TO_SERVICE and not client:
|
|
@@ -844,7 +854,7 @@ def run(
|
|
|
844
854
|
verbosity=hypothesis_verbosity,
|
|
845
855
|
)
|
|
846
856
|
event_stream = into_event_stream(
|
|
847
|
-
|
|
857
|
+
schema_or_location,
|
|
848
858
|
app=app,
|
|
849
859
|
base_url=base_url,
|
|
850
860
|
started_at=started_at,
|
|
@@ -876,13 +886,14 @@ def run(
|
|
|
876
886
|
stateful=stateful,
|
|
877
887
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
878
888
|
hypothesis_settings=hypothesis_settings,
|
|
889
|
+
generation_config=generation_config,
|
|
879
890
|
)
|
|
880
891
|
execute(
|
|
881
892
|
event_stream,
|
|
882
893
|
hypothesis_settings=hypothesis_settings,
|
|
883
894
|
workers_num=workers_num,
|
|
884
895
|
rate_limit=rate_limit,
|
|
885
|
-
|
|
896
|
+
show_trace=show_trace,
|
|
886
897
|
wait_for_schema=wait_for_schema,
|
|
887
898
|
validate_schema=validate_schema,
|
|
888
899
|
cassette_path=cassette_path,
|
|
@@ -904,7 +915,7 @@ def run(
|
|
|
904
915
|
)
|
|
905
916
|
|
|
906
917
|
|
|
907
|
-
def prepare_request_cert(cert:
|
|
918
|
+
def prepare_request_cert(cert: str | None, key: str | None) -> RequestCert | None:
|
|
908
919
|
if cert is not None and key is not None:
|
|
909
920
|
return cert, key
|
|
910
921
|
return cert
|
|
@@ -917,71 +928,72 @@ class LoaderConfig:
|
|
|
917
928
|
The main goal is to avoid too many parameters in function signatures.
|
|
918
929
|
"""
|
|
919
930
|
|
|
920
|
-
|
|
931
|
+
schema_or_location: str | dict[str, Any]
|
|
921
932
|
app: Any
|
|
922
|
-
base_url:
|
|
933
|
+
base_url: str | None
|
|
923
934
|
validate_schema: bool
|
|
924
935
|
skip_deprecated_operations: bool
|
|
925
|
-
data_generation_methods:
|
|
926
|
-
force_schema_version:
|
|
927
|
-
request_tls_verify:
|
|
928
|
-
request_cert:
|
|
929
|
-
wait_for_schema:
|
|
930
|
-
rate_limit:
|
|
936
|
+
data_generation_methods: tuple[DataGenerationMethod, ...]
|
|
937
|
+
force_schema_version: str | None
|
|
938
|
+
request_tls_verify: bool | str
|
|
939
|
+
request_cert: RequestCert | None
|
|
940
|
+
wait_for_schema: float | None
|
|
941
|
+
rate_limit: str | None
|
|
931
942
|
# Network request parameters
|
|
932
|
-
auth:
|
|
933
|
-
auth_type:
|
|
934
|
-
headers:
|
|
943
|
+
auth: tuple[str, str] | None
|
|
944
|
+
auth_type: str | None
|
|
945
|
+
headers: dict[str, str] | None
|
|
935
946
|
# Schema filters
|
|
936
|
-
endpoint:
|
|
937
|
-
method:
|
|
938
|
-
tag:
|
|
939
|
-
operation_id:
|
|
947
|
+
endpoint: Filter | None
|
|
948
|
+
method: Filter | None
|
|
949
|
+
tag: Filter | None
|
|
950
|
+
operation_id: Filter | None
|
|
940
951
|
|
|
941
952
|
|
|
942
953
|
def into_event_stream(
|
|
943
|
-
|
|
954
|
+
schema_or_location: str | dict[str, Any],
|
|
944
955
|
*,
|
|
945
956
|
app: Any,
|
|
946
|
-
base_url:
|
|
957
|
+
base_url: str | None,
|
|
947
958
|
started_at: str,
|
|
948
959
|
validate_schema: bool,
|
|
949
960
|
skip_deprecated_operations: bool,
|
|
950
|
-
data_generation_methods:
|
|
951
|
-
force_schema_version:
|
|
952
|
-
request_tls_verify:
|
|
953
|
-
request_cert:
|
|
961
|
+
data_generation_methods: tuple[DataGenerationMethod, ...],
|
|
962
|
+
force_schema_version: str | None,
|
|
963
|
+
request_tls_verify: bool | str,
|
|
964
|
+
request_cert: RequestCert | None,
|
|
954
965
|
# Network request parameters
|
|
955
|
-
auth:
|
|
956
|
-
auth_type:
|
|
957
|
-
headers:
|
|
958
|
-
request_timeout:
|
|
959
|
-
wait_for_schema:
|
|
966
|
+
auth: tuple[str, str] | None,
|
|
967
|
+
auth_type: str | None,
|
|
968
|
+
headers: dict[str, str] | None,
|
|
969
|
+
request_timeout: int | None,
|
|
970
|
+
wait_for_schema: float | None,
|
|
960
971
|
# Schema filters
|
|
961
|
-
endpoint:
|
|
962
|
-
method:
|
|
963
|
-
tag:
|
|
964
|
-
operation_id:
|
|
972
|
+
endpoint: Filter | None,
|
|
973
|
+
method: Filter | None,
|
|
974
|
+
tag: Filter | None,
|
|
975
|
+
operation_id: Filter | None,
|
|
965
976
|
# Runtime behavior
|
|
966
977
|
checks: Iterable[CheckFunction],
|
|
967
|
-
max_response_time:
|
|
978
|
+
max_response_time: int | None,
|
|
968
979
|
targets: Iterable[Target],
|
|
969
980
|
workers_num: int,
|
|
970
|
-
hypothesis_settings:
|
|
971
|
-
|
|
981
|
+
hypothesis_settings: hypothesis.settings | None,
|
|
982
|
+
generation_config: generation.GenerationConfig,
|
|
983
|
+
seed: int | None,
|
|
972
984
|
exit_first: bool,
|
|
973
|
-
max_failures:
|
|
974
|
-
rate_limit:
|
|
985
|
+
max_failures: int | None,
|
|
986
|
+
rate_limit: str | None,
|
|
975
987
|
dry_run: bool,
|
|
976
988
|
store_interactions: bool,
|
|
977
|
-
stateful:
|
|
989
|
+
stateful: Stateful | None,
|
|
978
990
|
stateful_recursion_limit: int,
|
|
979
991
|
) -> Generator[events.ExecutionEvent, None, None]:
|
|
980
992
|
try:
|
|
981
993
|
if app is not None:
|
|
982
994
|
app = load_app(app)
|
|
983
995
|
config = LoaderConfig(
|
|
984
|
-
|
|
996
|
+
schema_or_location=schema_or_location,
|
|
985
997
|
app=app,
|
|
986
998
|
base_url=base_url,
|
|
987
999
|
validate_schema=validate_schema,
|
|
@@ -1022,6 +1034,7 @@ def into_event_stream(
|
|
|
1022
1034
|
stateful=stateful,
|
|
1023
1035
|
stateful_recursion_limit=stateful_recursion_limit,
|
|
1024
1036
|
hypothesis_settings=hypothesis_settings,
|
|
1037
|
+
generation_config=generation_config,
|
|
1025
1038
|
).execute()
|
|
1026
1039
|
except SchemaError as error:
|
|
1027
1040
|
yield events.InternalError.from_schema_error(error)
|
|
@@ -1033,7 +1046,7 @@ def load_schema(config: LoaderConfig) -> BaseSchema:
|
|
|
1033
1046
|
"""Automatically load API schema."""
|
|
1034
1047
|
first: Callable[[LoaderConfig], BaseSchema]
|
|
1035
1048
|
second: Callable[[LoaderConfig], BaseSchema]
|
|
1036
|
-
if is_probably_graphql(config.
|
|
1049
|
+
if is_probably_graphql(config.schema_or_location):
|
|
1037
1050
|
# Try GraphQL first, then fallback to Open API
|
|
1038
1051
|
first, second = (_load_graphql_schema, _load_openapi_schema)
|
|
1039
1052
|
else:
|
|
@@ -1049,9 +1062,10 @@ def should_try_more(exc: SchemaError) -> bool:
|
|
|
1049
1062
|
return not isinstance(exc.__cause__, requests.exceptions.ConnectionError)
|
|
1050
1063
|
|
|
1051
1064
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1065
|
+
Loader = Callable[[LoaderConfig], "BaseSchema"]
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _try_load_schema(config: LoaderConfig, first: Loader, second: Loader) -> BaseSchema:
|
|
1055
1069
|
from urllib3.exceptions import InsecureRequestWarning
|
|
1056
1070
|
|
|
1057
1071
|
with warnings.catch_warnings():
|
|
@@ -1060,38 +1074,51 @@ def _try_load_schema(
|
|
|
1060
1074
|
return first(config)
|
|
1061
1075
|
except SchemaError as exc:
|
|
1062
1076
|
if should_try_more(exc):
|
|
1063
|
-
|
|
1077
|
+
try:
|
|
1064
1078
|
return second(config)
|
|
1079
|
+
except Exception as second_exc:
|
|
1080
|
+
if is_specific_exception(second, second_exc):
|
|
1081
|
+
raise second_exc
|
|
1065
1082
|
# Re-raise the original error
|
|
1066
1083
|
raise exc
|
|
1067
1084
|
|
|
1068
1085
|
|
|
1086
|
+
def is_specific_exception(loader: Loader, exc: Exception) -> bool:
|
|
1087
|
+
return (
|
|
1088
|
+
loader is _load_graphql_schema
|
|
1089
|
+
and isinstance(exc, SchemaError)
|
|
1090
|
+
and exc.type == SchemaErrorType.GRAPHQL_INVALID_SCHEMA
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
|
|
1069
1094
|
def _load_graphql_schema(config: LoaderConfig) -> GraphQLSchema:
|
|
1070
|
-
loader = detect_loader(config.
|
|
1095
|
+
loader = detect_loader(config.schema_or_location, config.app, is_openapi=False)
|
|
1071
1096
|
kwargs = get_graphql_loader_kwargs(loader, config)
|
|
1072
|
-
return loader(config.
|
|
1097
|
+
return loader(config.schema_or_location, **kwargs)
|
|
1073
1098
|
|
|
1074
1099
|
|
|
1075
1100
|
def _load_openapi_schema(config: LoaderConfig) -> BaseSchema:
|
|
1076
|
-
loader = detect_loader(config.
|
|
1101
|
+
loader = detect_loader(config.schema_or_location, config.app, is_openapi=True)
|
|
1077
1102
|
kwargs = get_loader_kwargs(loader, config)
|
|
1078
|
-
return loader(config.
|
|
1103
|
+
return loader(config.schema_or_location, **kwargs)
|
|
1079
1104
|
|
|
1080
1105
|
|
|
1081
|
-
def detect_loader(
|
|
1106
|
+
def detect_loader(schema_or_location: str | dict[str, Any], app: Any, is_openapi: bool) -> Callable:
|
|
1082
1107
|
"""Detect API schema loader."""
|
|
1083
|
-
if
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1108
|
+
if isinstance(schema_or_location, str):
|
|
1109
|
+
if file_exists(schema_or_location):
|
|
1110
|
+
# If there is an existing file with the given name,
|
|
1111
|
+
# then it is likely that the user wants to load API schema from there
|
|
1112
|
+
return oas_loaders.from_path if is_openapi else gql_loaders.from_path # type: ignore
|
|
1113
|
+
if app is not None and not urlparse(schema_or_location).netloc:
|
|
1114
|
+
# App is passed & location is relative
|
|
1115
|
+
return oas_loaders.get_loader_for_app(app) if is_openapi else gql_loaders.get_loader_for_app(app)
|
|
1116
|
+
# Default behavior
|
|
1117
|
+
return oas_loaders.from_uri if is_openapi else gql_loaders.from_url # type: ignore
|
|
1118
|
+
return oas_loaders.from_dict if is_openapi else gql_loaders.from_dict # type: ignore
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> dict[str, Any]:
|
|
1095
1122
|
"""Detect the proper set of parameters for a loader."""
|
|
1096
1123
|
# These kwargs are shared by all loaders
|
|
1097
1124
|
kwargs = {
|
|
@@ -1107,7 +1134,7 @@ def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> Dict[str, Any]:
|
|
|
1107
1134
|
"data_generation_methods": config.data_generation_methods,
|
|
1108
1135
|
"rate_limit": config.rate_limit,
|
|
1109
1136
|
}
|
|
1110
|
-
if loader
|
|
1137
|
+
if loader not in (oas_loaders.from_path, oas_loaders.from_dict):
|
|
1111
1138
|
kwargs["headers"] = config.headers
|
|
1112
1139
|
if loader in (oas_loaders.from_uri, oas_loaders.from_aiohttp):
|
|
1113
1140
|
_add_requests_kwargs(kwargs, config)
|
|
@@ -1117,7 +1144,7 @@ def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> Dict[str, Any]:
|
|
|
1117
1144
|
def get_graphql_loader_kwargs(
|
|
1118
1145
|
loader: Callable,
|
|
1119
1146
|
config: LoaderConfig,
|
|
1120
|
-
) ->
|
|
1147
|
+
) -> dict[str, Any]:
|
|
1121
1148
|
"""Detect the proper set of parameters for a loader."""
|
|
1122
1149
|
# These kwargs are shared by all loaders
|
|
1123
1150
|
kwargs = {
|
|
@@ -1126,14 +1153,14 @@ def get_graphql_loader_kwargs(
|
|
|
1126
1153
|
"data_generation_methods": config.data_generation_methods,
|
|
1127
1154
|
"rate_limit": config.rate_limit,
|
|
1128
1155
|
}
|
|
1129
|
-
if loader
|
|
1156
|
+
if loader not in (gql_loaders.from_path, gql_loaders.from_dict):
|
|
1130
1157
|
kwargs["headers"] = config.headers
|
|
1131
1158
|
if loader is gql_loaders.from_url:
|
|
1132
1159
|
_add_requests_kwargs(kwargs, config)
|
|
1133
1160
|
return kwargs
|
|
1134
1161
|
|
|
1135
1162
|
|
|
1136
|
-
def _add_requests_kwargs(kwargs:
|
|
1163
|
+
def _add_requests_kwargs(kwargs: dict[str, Any], config: LoaderConfig) -> None:
|
|
1137
1164
|
kwargs["verify"] = config.request_tls_verify
|
|
1138
1165
|
if config.request_cert is not None:
|
|
1139
1166
|
kwargs["cert"] = config.request_cert
|
|
@@ -1143,12 +1170,16 @@ def _add_requests_kwargs(kwargs: Dict[str, Any], config: LoaderConfig) -> None:
|
|
|
1143
1170
|
kwargs["wait_for_schema"] = config.wait_for_schema
|
|
1144
1171
|
|
|
1145
1172
|
|
|
1146
|
-
def is_probably_graphql(
|
|
1173
|
+
def is_probably_graphql(schema_or_location: str | dict[str, Any]) -> bool:
|
|
1147
1174
|
"""Detect whether it is likely that the given location is a GraphQL endpoint."""
|
|
1148
|
-
|
|
1175
|
+
if isinstance(schema_or_location, str):
|
|
1176
|
+
return schema_or_location.endswith(("/graphql", "/graphql/", ".graphql", ".gql"))
|
|
1177
|
+
return "__schema" in schema_or_location or (
|
|
1178
|
+
"data" in schema_or_location and "__schema" in schema_or_location["data"]
|
|
1179
|
+
)
|
|
1149
1180
|
|
|
1150
1181
|
|
|
1151
|
-
def check_auth(auth:
|
|
1182
|
+
def check_auth(auth: tuple[str, str] | None, headers: dict[str, str]) -> None:
|
|
1152
1183
|
if auth is not None and "authorization" in {header.lower() for header in headers}:
|
|
1153
1184
|
raise click.BadParameter(
|
|
1154
1185
|
"The `--auth` and `--header` options were both used to set "
|
|
@@ -1199,30 +1230,30 @@ def execute(
|
|
|
1199
1230
|
*,
|
|
1200
1231
|
hypothesis_settings: hypothesis.settings,
|
|
1201
1232
|
workers_num: int,
|
|
1202
|
-
rate_limit:
|
|
1203
|
-
|
|
1204
|
-
wait_for_schema:
|
|
1233
|
+
rate_limit: str | None,
|
|
1234
|
+
show_trace: bool,
|
|
1235
|
+
wait_for_schema: float | None,
|
|
1205
1236
|
validate_schema: bool,
|
|
1206
|
-
cassette_path:
|
|
1237
|
+
cassette_path: click.utils.LazyFile | None,
|
|
1207
1238
|
cassette_preserve_exact_body_bytes: bool,
|
|
1208
|
-
junit_xml:
|
|
1239
|
+
junit_xml: click.utils.LazyFile | None,
|
|
1209
1240
|
verbosity: int,
|
|
1210
1241
|
code_sample_style: CodeSampleStyle,
|
|
1211
|
-
data_generation_methods:
|
|
1212
|
-
debug_output_file:
|
|
1242
|
+
data_generation_methods: tuple[DataGenerationMethod, ...],
|
|
1243
|
+
debug_output_file: click.utils.LazyFile | None,
|
|
1213
1244
|
sanitize_output: bool,
|
|
1214
1245
|
host_data: service.hosts.HostData,
|
|
1215
|
-
client:
|
|
1216
|
-
report:
|
|
1246
|
+
client: ServiceClient | None,
|
|
1247
|
+
report: ReportToService | click.utils.LazyFile | None,
|
|
1217
1248
|
telemetry: bool,
|
|
1218
|
-
api_name:
|
|
1249
|
+
api_name: str | None,
|
|
1219
1250
|
location: str,
|
|
1220
|
-
base_url:
|
|
1251
|
+
base_url: str | None,
|
|
1221
1252
|
started_at: str,
|
|
1222
1253
|
) -> None:
|
|
1223
1254
|
"""Execute a prepared runner by drawing events from it and passing to a proper handler."""
|
|
1224
|
-
handlers:
|
|
1225
|
-
report_context:
|
|
1255
|
+
handlers: list[EventHandler] = []
|
|
1256
|
+
report_context: ServiceReportContext | FileReportContext | None = None
|
|
1226
1257
|
report_queue: Queue
|
|
1227
1258
|
if client:
|
|
1228
1259
|
# If API name is specified, validate it
|
|
@@ -1270,7 +1301,7 @@ def execute(
|
|
|
1270
1301
|
hypothesis_settings=hypothesis_settings,
|
|
1271
1302
|
workers_num=workers_num,
|
|
1272
1303
|
rate_limit=rate_limit,
|
|
1273
|
-
|
|
1304
|
+
show_trace=show_trace,
|
|
1274
1305
|
wait_for_schema=wait_for_schema,
|
|
1275
1306
|
validate_schema=validate_schema,
|
|
1276
1307
|
cassette_path=cassette_path.name if cassette_path is not None else None,
|
|
@@ -1362,7 +1393,7 @@ def handle_service_error(exc: requests.HTTPError, api_name: str) -> NoReturn:
|
|
|
1362
1393
|
elif response.status_code == 404:
|
|
1363
1394
|
error_message(f"API with name `{api_name}` not found!")
|
|
1364
1395
|
else:
|
|
1365
|
-
output.default.display_service_error(service.Error(exc))
|
|
1396
|
+
output.default.display_service_error(service.Error(exc), message_prefix="❌ ")
|
|
1366
1397
|
sys.exit(1)
|
|
1367
1398
|
|
|
1368
1399
|
|
|
@@ -1392,15 +1423,15 @@ def get_exit_code(event: events.ExecutionEvent) -> int:
|
|
|
1392
1423
|
def replay(
|
|
1393
1424
|
ctx: click.Context,
|
|
1394
1425
|
cassette_path: str,
|
|
1395
|
-
id_:
|
|
1396
|
-
status:
|
|
1397
|
-
uri:
|
|
1398
|
-
method:
|
|
1426
|
+
id_: str | None,
|
|
1427
|
+
status: str | None = None,
|
|
1428
|
+
uri: str | None = None,
|
|
1429
|
+
method: str | None = None,
|
|
1399
1430
|
no_color: bool = False,
|
|
1400
1431
|
verbosity: int = 0,
|
|
1401
1432
|
request_tls_verify: bool = True,
|
|
1402
|
-
request_cert:
|
|
1403
|
-
request_cert_key:
|
|
1433
|
+
request_cert: str | None = None,
|
|
1434
|
+
request_cert_key: str | None = None,
|
|
1404
1435
|
force_color: bool = False,
|
|
1405
1436
|
) -> None:
|
|
1406
1437
|
"""Replay a cassette.
|
|
@@ -1467,7 +1498,7 @@ def upload(
|
|
|
1467
1498
|
hosts_file: str,
|
|
1468
1499
|
request_tls_verify: bool = True,
|
|
1469
1500
|
schemathesis_io_url: str = service.DEFAULT_URL,
|
|
1470
|
-
schemathesis_io_token:
|
|
1501
|
+
schemathesis_io_token: str | None = None,
|
|
1471
1502
|
) -> None:
|
|
1472
1503
|
"""Upload report to Schemathesis.io."""
|
|
1473
1504
|
from ..service.client import ServiceClient
|
|
@@ -1579,7 +1610,7 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
|
|
|
1579
1610
|
|
|
1580
1611
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
1581
1612
|
def after_init_cli_run_handlers(
|
|
1582
|
-
context: HookContext, handlers:
|
|
1613
|
+
context: HookContext, handlers: list[EventHandler], execution_context: ExecutionContext
|
|
1583
1614
|
) -> None:
|
|
1584
1615
|
"""Called after CLI hooks are initialized.
|
|
1585
1616
|
|
|
@@ -1588,7 +1619,7 @@ def after_init_cli_run_handlers(
|
|
|
1588
1619
|
|
|
1589
1620
|
|
|
1590
1621
|
@HookDispatcher.register_spec([HookScope.GLOBAL])
|
|
1591
|
-
def process_call_kwargs(context: HookContext, case: Case, kwargs:
|
|
1622
|
+
def process_call_kwargs(context: HookContext, case: Case, kwargs: dict[str, Any]) -> None:
|
|
1592
1623
|
"""Called before every network call in CLI tests.
|
|
1593
1624
|
|
|
1594
1625
|
Aims to modify the argument passed to `case.call` / `case.call_wsgi` / `case.call_asgi`.
|