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.
Files changed (95) hide show
  1. schemathesis/__init__.py +1 -1
  2. schemathesis/_compat.py +2 -18
  3. schemathesis/_dependency_versions.py +1 -6
  4. schemathesis/_hypothesis.py +15 -12
  5. schemathesis/_lazy_import.py +3 -2
  6. schemathesis/_xml.py +12 -11
  7. schemathesis/auths.py +88 -81
  8. schemathesis/checks.py +4 -4
  9. schemathesis/cli/__init__.py +202 -171
  10. schemathesis/cli/callbacks.py +29 -32
  11. schemathesis/cli/cassettes.py +25 -25
  12. schemathesis/cli/context.py +18 -12
  13. schemathesis/cli/junitxml.py +2 -2
  14. schemathesis/cli/options.py +10 -11
  15. schemathesis/cli/output/default.py +64 -34
  16. schemathesis/code_samples.py +10 -10
  17. schemathesis/constants.py +1 -1
  18. schemathesis/contrib/unique_data.py +2 -2
  19. schemathesis/exceptions.py +55 -42
  20. schemathesis/extra/_aiohttp.py +2 -2
  21. schemathesis/extra/_flask.py +2 -2
  22. schemathesis/extra/_server.py +3 -2
  23. schemathesis/extra/pytest_plugin.py +10 -10
  24. schemathesis/failures.py +16 -16
  25. schemathesis/filters.py +40 -41
  26. schemathesis/fixups/__init__.py +4 -3
  27. schemathesis/fixups/fast_api.py +5 -4
  28. schemathesis/generation/__init__.py +16 -4
  29. schemathesis/hooks.py +25 -25
  30. schemathesis/internal/jsonschema.py +4 -3
  31. schemathesis/internal/transformation.py +3 -2
  32. schemathesis/lazy.py +39 -31
  33. schemathesis/loaders.py +8 -8
  34. schemathesis/models.py +128 -126
  35. schemathesis/parameters.py +6 -5
  36. schemathesis/runner/__init__.py +107 -81
  37. schemathesis/runner/events.py +37 -26
  38. schemathesis/runner/impl/core.py +86 -81
  39. schemathesis/runner/impl/solo.py +19 -15
  40. schemathesis/runner/impl/threadpool.py +40 -22
  41. schemathesis/runner/serialization.py +67 -40
  42. schemathesis/sanitization.py +18 -20
  43. schemathesis/schemas.py +83 -72
  44. schemathesis/serializers.py +39 -30
  45. schemathesis/service/ci.py +20 -21
  46. schemathesis/service/client.py +29 -9
  47. schemathesis/service/constants.py +1 -0
  48. schemathesis/service/events.py +2 -2
  49. schemathesis/service/hosts.py +8 -7
  50. schemathesis/service/metadata.py +5 -0
  51. schemathesis/service/models.py +22 -4
  52. schemathesis/service/report.py +15 -15
  53. schemathesis/service/serialization.py +23 -27
  54. schemathesis/service/usage.py +8 -7
  55. schemathesis/specs/graphql/loaders.py +31 -24
  56. schemathesis/specs/graphql/nodes.py +3 -2
  57. schemathesis/specs/graphql/scalars.py +26 -2
  58. schemathesis/specs/graphql/schemas.py +38 -34
  59. schemathesis/specs/openapi/_hypothesis.py +62 -44
  60. schemathesis/specs/openapi/checks.py +10 -10
  61. schemathesis/specs/openapi/converter.py +10 -9
  62. schemathesis/specs/openapi/definitions.py +2 -2
  63. schemathesis/specs/openapi/examples.py +22 -21
  64. schemathesis/specs/openapi/expressions/nodes.py +5 -4
  65. schemathesis/specs/openapi/expressions/parser.py +7 -6
  66. schemathesis/specs/openapi/filters.py +6 -6
  67. schemathesis/specs/openapi/formats.py +2 -2
  68. schemathesis/specs/openapi/links.py +19 -21
  69. schemathesis/specs/openapi/loaders.py +133 -78
  70. schemathesis/specs/openapi/negative/__init__.py +16 -11
  71. schemathesis/specs/openapi/negative/mutations.py +11 -10
  72. schemathesis/specs/openapi/parameters.py +20 -19
  73. schemathesis/specs/openapi/references.py +21 -20
  74. schemathesis/specs/openapi/schemas.py +97 -84
  75. schemathesis/specs/openapi/security.py +25 -24
  76. schemathesis/specs/openapi/serialization.py +20 -23
  77. schemathesis/specs/openapi/stateful/__init__.py +12 -11
  78. schemathesis/specs/openapi/stateful/links.py +7 -7
  79. schemathesis/specs/openapi/utils.py +4 -3
  80. schemathesis/specs/openapi/validation.py +3 -2
  81. schemathesis/stateful/__init__.py +15 -16
  82. schemathesis/stateful/state_machine.py +9 -9
  83. schemathesis/targets.py +3 -3
  84. schemathesis/throttling.py +2 -2
  85. schemathesis/transports/auth.py +2 -2
  86. schemathesis/transports/content_types.py +5 -0
  87. schemathesis/transports/headers.py +3 -2
  88. schemathesis/transports/responses.py +1 -1
  89. schemathesis/utils.py +7 -10
  90. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/METADATA +12 -13
  91. schemathesis-3.22.1.dist-info/RECORD +130 -0
  92. schemathesis-3.21.2.dist-info/RECORD +0 -130
  93. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/WHEEL +0 -0
  94. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/entry_points.txt +0 -0
  95. {schemathesis-3.21.2.dist-info → schemathesis-3.22.1.dist-info}/licenses/LICENSE +0 -0
@@ -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, Dict, Generator, Iterable, List, NoReturn, Optional, Tuple, Union, cast, TYPE_CHECKING
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: Tuple[Callable, ...]) -> Tuple[str, ...]:
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: Optional[str] = None) -> None:
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: Optional[str]
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="[DEPRECATED] Saves the test outcomes in a VCR-compatible format.",
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
- cls=DeprecatedOption,
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: Optional[str],
660
- auth: Optional[Tuple[str, str]],
669
+ api_name: str | None,
670
+ auth: tuple[str, str] | None,
661
671
  auth_type: str,
662
- headers: Dict[str, str],
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: Tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
667
- max_response_time: Optional[int] = None,
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: Optional[int] = None,
680
+ max_failures: int | None = None,
671
681
  dry_run: bool = False,
672
- endpoints: Optional[Filter] = None,
673
- methods: Optional[Filter] = None,
674
- tags: Optional[Filter] = None,
675
- operation_ids: Optional[Filter] = None,
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: Optional[str] = None,
678
- app: Optional[str] = None,
679
- request_timeout: Optional[int] = None,
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: Optional[str] = None,
682
- request_cert_key: Optional[str] = None,
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: Optional[click.utils.LazyFile] = None,
686
- debug_output_file: Optional[click.utils.LazyFile] = None,
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: Optional[click.utils.LazyFile] = None,
700
+ cassette_path: click.utils.LazyFile | None = None,
690
701
  cassette_preserve_exact_body_bytes: bool = False,
691
- store_network_log: Optional[click.utils.LazyFile] = None,
692
- wait_for_schema: Optional[float] = None,
693
- fixups: Tuple[str] = (), # type: ignore
694
- rate_limit: Optional[str] = None,
695
- stateful: Optional[Stateful] = None,
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: Optional[str] = None,
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: Optional[str] = None,
702
- hypothesis_deadline: Optional[Union[int, NotSet]] = None,
703
- hypothesis_derandomize: Optional[bool] = None,
704
- hypothesis_max_examples: Optional[int] = None,
705
- hypothesis_phases: Optional[List[Phase]] = None,
706
- hypothesis_report_multiple_bugs: Optional[bool] = None,
707
- hypothesis_suppress_health_check: Optional[List[HealthCheck]] = None,
708
- hypothesis_seed: Optional[int] = None,
709
- hypothesis_verbosity: Optional[hypothesis.Verbosity] = None,
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: Optional[str] = None,
713
- schemathesis_io_token: Optional[str] = None,
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: Optional[List[hypothesis.Phase]] = None
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: Optional[List[hypothesis.HealthCheck]] = None
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
- report: Optional[Union[ReportToService, click.utils.LazyFile]]
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
- click.secho("\nTo authenticate:")
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
- schema = details.location
808
- base_url = base_url or details.base_url
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
- schema,
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
- show_errors_tracebacks=show_errors_tracebacks,
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: Optional[str], key: Optional[str]) -> Optional[RequestCert]:
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
- schema_location: str
931
+ schema_or_location: str | dict[str, Any]
921
932
  app: Any
922
- base_url: Optional[str]
933
+ base_url: str | None
923
934
  validate_schema: bool
924
935
  skip_deprecated_operations: bool
925
- data_generation_methods: Tuple[DataGenerationMethod, ...]
926
- force_schema_version: Optional[str]
927
- request_tls_verify: Union[bool, str]
928
- request_cert: Optional[RequestCert]
929
- wait_for_schema: Optional[float]
930
- rate_limit: Optional[str]
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: Optional[Tuple[str, str]]
933
- auth_type: Optional[str]
934
- headers: Optional[Dict[str, str]]
943
+ auth: tuple[str, str] | None
944
+ auth_type: str | None
945
+ headers: dict[str, str] | None
935
946
  # Schema filters
936
- endpoint: Optional[Filter]
937
- method: Optional[Filter]
938
- tag: Optional[Filter]
939
- operation_id: Optional[Filter]
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
- schema_location: str,
954
+ schema_or_location: str | dict[str, Any],
944
955
  *,
945
956
  app: Any,
946
- base_url: Optional[str],
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: Tuple[DataGenerationMethod, ...],
951
- force_schema_version: Optional[str],
952
- request_tls_verify: Union[bool, str],
953
- request_cert: Optional[RequestCert],
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: Optional[Tuple[str, str]],
956
- auth_type: Optional[str],
957
- headers: Optional[Dict[str, str]],
958
- request_timeout: Optional[int],
959
- wait_for_schema: Optional[float],
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: Optional[Filter],
962
- method: Optional[Filter],
963
- tag: Optional[Filter],
964
- operation_id: Optional[Filter],
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: Optional[int],
978
+ max_response_time: int | None,
968
979
  targets: Iterable[Target],
969
980
  workers_num: int,
970
- hypothesis_settings: Optional[hypothesis.settings],
971
- seed: Optional[int],
981
+ hypothesis_settings: hypothesis.settings | None,
982
+ generation_config: generation.GenerationConfig,
983
+ seed: int | None,
972
984
  exit_first: bool,
973
- max_failures: Optional[int],
974
- rate_limit: Optional[str],
985
+ max_failures: int | None,
986
+ rate_limit: str | None,
975
987
  dry_run: bool,
976
988
  store_interactions: bool,
977
- stateful: Optional[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
- schema_location=schema_location,
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.schema_location):
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
- def _try_load_schema(
1053
- config: LoaderConfig, first: Callable[[LoaderConfig], BaseSchema], second: Callable[[LoaderConfig], BaseSchema]
1054
- ) -> BaseSchema:
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
- with suppress(Exception):
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.schema_location, config.app, is_openapi=False)
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.schema_location, **kwargs)
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.schema_location, config.app, is_openapi=True)
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.schema_location, **kwargs)
1103
+ return loader(config.schema_or_location, **kwargs)
1079
1104
 
1080
1105
 
1081
- def detect_loader(schema_location: str, app: Any, is_openapi: bool) -> Callable:
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 file_exists(schema_location):
1084
- # If there is an existing file with the given name,
1085
- # then it is likely that the user wants to load API schema from there
1086
- return oas_loaders.from_path if is_openapi else gql_loaders.from_path # type: ignore
1087
- if app is not None and not urlparse(schema_location).netloc:
1088
- # App is passed & location is relative
1089
- return oas_loaders.get_loader_for_app(app) if is_openapi else gql_loaders.get_loader_for_app(app)
1090
- # Default behavior
1091
- return oas_loaders.from_uri if is_openapi else gql_loaders.from_url # type: ignore
1092
-
1093
-
1094
- def get_loader_kwargs(loader: Callable, config: LoaderConfig) -> Dict[str, Any]:
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 is not oas_loaders.from_path:
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
- ) -> Dict[str, Any]:
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 is not gql_loaders.from_path:
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: Dict[str, Any], config: LoaderConfig) -> None:
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(location: str) -> bool:
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
- return location.endswith(("/graphql", "/graphql/"))
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: Optional[Tuple[str, str]], headers: Dict[str, str]) -> None:
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: Optional[str],
1203
- show_errors_tracebacks: bool,
1204
- wait_for_schema: Optional[float],
1233
+ rate_limit: str | None,
1234
+ show_trace: bool,
1235
+ wait_for_schema: float | None,
1205
1236
  validate_schema: bool,
1206
- cassette_path: Optional[click.utils.LazyFile],
1237
+ cassette_path: click.utils.LazyFile | None,
1207
1238
  cassette_preserve_exact_body_bytes: bool,
1208
- junit_xml: Optional[click.utils.LazyFile],
1239
+ junit_xml: click.utils.LazyFile | None,
1209
1240
  verbosity: int,
1210
1241
  code_sample_style: CodeSampleStyle,
1211
- data_generation_methods: Tuple[DataGenerationMethod, ...],
1212
- debug_output_file: Optional[click.utils.LazyFile],
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: Optional[ServiceClient],
1216
- report: Optional[Union[ReportToService, click.utils.LazyFile]],
1246
+ client: ServiceClient | None,
1247
+ report: ReportToService | click.utils.LazyFile | None,
1217
1248
  telemetry: bool,
1218
- api_name: Optional[str],
1249
+ api_name: str | None,
1219
1250
  location: str,
1220
- base_url: Optional[str],
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: List[EventHandler] = []
1225
- report_context: Optional[Union[ServiceReportContext, FileReportContext]] = None
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
- show_errors_tracebacks=show_errors_tracebacks,
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_: Optional[str],
1396
- status: Optional[str] = None,
1397
- uri: Optional[str] = None,
1398
- method: Optional[str] = None,
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: Optional[str] = None,
1403
- request_cert_key: Optional[str] = None,
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: Optional[str] = None,
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: List[EventHandler], execution_context: ExecutionContext
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: Dict[str, Any]) -> None:
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`.