schemathesis 3.25.6__py3-none-any.whl → 3.26.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 (42) hide show
  1. schemathesis/_dependency_versions.py +1 -0
  2. schemathesis/_hypothesis.py +11 -1
  3. schemathesis/_xml.py +1 -0
  4. schemathesis/auths.py +1 -0
  5. schemathesis/cli/__init__.py +26 -18
  6. schemathesis/cli/cassettes.py +4 -4
  7. schemathesis/cli/context.py +4 -1
  8. schemathesis/cli/output/default.py +154 -39
  9. schemathesis/cli/output/short.py +4 -0
  10. schemathesis/experimental/__init__.py +7 -0
  11. schemathesis/filters.py +1 -0
  12. schemathesis/models.py +3 -0
  13. schemathesis/parameters.py +1 -0
  14. schemathesis/runner/__init__.py +12 -0
  15. schemathesis/runner/events.py +12 -0
  16. schemathesis/runner/impl/core.py +29 -2
  17. schemathesis/runner/probes.py +1 -0
  18. schemathesis/runner/serialization.py +6 -2
  19. schemathesis/schemas.py +1 -0
  20. schemathesis/serializers.py +1 -1
  21. schemathesis/service/client.py +35 -2
  22. schemathesis/service/extensions.py +224 -0
  23. schemathesis/service/hosts.py +1 -0
  24. schemathesis/service/metadata.py +24 -0
  25. schemathesis/service/models.py +210 -2
  26. schemathesis/service/serialization.py +31 -1
  27. schemathesis/specs/graphql/schemas.py +4 -0
  28. schemathesis/specs/openapi/__init__.py +1 -0
  29. schemathesis/specs/openapi/_hypothesis.py +11 -0
  30. schemathesis/specs/openapi/expressions/__init__.py +1 -0
  31. schemathesis/specs/openapi/expressions/lexer.py +1 -0
  32. schemathesis/specs/openapi/expressions/nodes.py +1 -0
  33. schemathesis/specs/openapi/links.py +1 -0
  34. schemathesis/specs/openapi/media_types.py +34 -0
  35. schemathesis/specs/openapi/negative/mutations.py +1 -0
  36. schemathesis/specs/openapi/schemas.py +1 -0
  37. schemathesis/specs/openapi/security.py +5 -1
  38. {schemathesis-3.25.6.dist-info → schemathesis-3.26.1.dist-info}/METADATA +8 -5
  39. {schemathesis-3.25.6.dist-info → schemathesis-3.26.1.dist-info}/RECORD +42 -40
  40. {schemathesis-3.25.6.dist-info → schemathesis-3.26.1.dist-info}/WHEEL +0 -0
  41. {schemathesis-3.25.6.dist-info → schemathesis-3.26.1.dist-info}/entry_points.txt +0 -0
  42. {schemathesis-3.25.6.dist-info → schemathesis-3.26.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,4 +1,5 @@
1
1
  """Compatibility flags based on installed dependency versions."""
2
+
2
3
  from packaging import version
3
4
 
4
5
  from importlib import metadata
@@ -1,4 +1,5 @@
1
1
  """High-level API for creating Hypothesis tests."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  import asyncio
@@ -76,7 +77,16 @@ def create_test(
76
77
  wrapped_test.hypothesis.inner_test = make_async_test(test) # type: ignore
77
78
  setup_default_deadline(wrapped_test)
78
79
  if settings is not None:
79
- wrapped_test = settings(wrapped_test)
80
+ existing_settings = _get_hypothesis_settings(wrapped_test)
81
+ if existing_settings is not None:
82
+ # Merge the user-provided settings with the current ones
83
+ default = hypothesis.settings.default
84
+ wrapped_test._hypothesis_internal_use_settings = hypothesis.settings(
85
+ wrapped_test._hypothesis_internal_use_settings,
86
+ **{item: value for item, value in settings.__dict__.items() if value != getattr(default, item)},
87
+ )
88
+ else:
89
+ wrapped_test = settings(wrapped_test)
80
90
  existing_settings = _get_hypothesis_settings(wrapped_test)
81
91
  if existing_settings is not None:
82
92
  existing_settings = remove_explain_phase(existing_settings)
schemathesis/_xml.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """XML serialization."""
2
+
2
3
  from __future__ import annotations
3
4
  from io import StringIO
4
5
  from typing import Any, Dict, List, Union
schemathesis/auths.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Support for custom API authentication mechanisms."""
2
+
2
3
  from __future__ import annotations
3
4
  import inspect
4
5
  import threading
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import base64
3
4
  import enum
4
5
  import io
@@ -10,47 +11,45 @@ from collections import defaultdict
10
11
  from dataclasses import dataclass
11
12
  from enum import Enum
12
13
  from queue import Queue
13
- from typing import Any, Callable, Generator, Iterable, NoReturn, cast, TYPE_CHECKING
14
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, NoReturn, cast
14
15
  from urllib.parse import urlparse
15
16
 
16
17
  import click
17
18
 
18
19
  from .. import checks as checks_module
19
- from .. import contrib, experimental, generation
20
+ from .. import contrib, experimental, generation, runner, service
20
21
  from .. import fixups as _fixups
21
- from .. import runner, service
22
22
  from .. import targets as targets_module
23
+ from .._override import CaseOverride
23
24
  from ..code_samples import CodeSampleStyle
24
- from .constants import HealthCheck, Phase, Verbosity
25
- from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
26
25
  from ..constants import (
27
26
  API_NAME_ENV_VAR,
28
27
  BASE_URL_ENV_VAR,
29
28
  DEFAULT_RESPONSE_TIMEOUT,
30
29
  DEFAULT_STATEFUL_RECURSION_LIMIT,
30
+ EXTENSIONS_DOCUMENTATION_URL,
31
31
  HOOKS_MODULE_ENV_VAR,
32
32
  HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER,
33
- WAIT_FOR_SCHEMA_ENV_VAR,
34
- EXTENSIONS_DOCUMENTATION_URL,
35
33
  ISSUE_TRACKER_URL,
34
+ WAIT_FOR_SCHEMA_ENV_VAR,
36
35
  )
37
- from ..exceptions import SchemaError, extract_nth_traceback, SchemaErrorType
36
+ from ..exceptions import SchemaError, SchemaErrorType, extract_nth_traceback
38
37
  from ..fixups import ALL_FIXUPS
39
- from ..loaders import load_app, load_yaml
40
- from .._override import CaseOverride
41
- from ..transports.auth import get_requests_auth
38
+ from ..generation import DEFAULT_DATA_GENERATION_METHODS, DataGenerationMethod
42
39
  from ..hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, HookScope
40
+ from ..internal.datetime import current_datetime
41
+ from ..internal.validation import file_exists
42
+ from ..loaders import load_app, load_yaml
43
43
  from ..models import Case, CheckFunction
44
44
  from ..runner import events, prepare_hypothesis_settings, probes
45
45
  from ..specs.graphql import loaders as gql_loaders
46
46
  from ..specs.openapi import loaders as oas_loaders
47
47
  from ..stateful import Stateful
48
48
  from ..targets import Target
49
+ from ..transports.auth import get_requests_auth
49
50
  from ..types import Filter, PathLike, RequestCert
50
- from ..internal.datetime import current_datetime
51
- from ..internal.validation import file_exists
52
51
  from . import callbacks, cassettes, output
53
- from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
52
+ from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
54
53
  from .context import ExecutionContext, FileReportContext, ServiceReportContext
55
54
  from .debug import DebugOutputHandler
56
55
  from .junitxml import JunitXMLHandler
@@ -60,8 +59,9 @@ from .sanitization import SanitizationHandler
60
59
  if TYPE_CHECKING:
61
60
  import hypothesis
62
61
  import requests
63
- from ..service.client import ServiceClient
62
+
64
63
  from ..schemas import BaseSchema
64
+ from ..service.client import ServiceClient
65
65
  from ..specs.graphql.schemas import GraphQLSchema
66
66
  from .handlers import EventHandler
67
67
 
@@ -681,8 +681,9 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
681
681
  @click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
682
682
  @click.option(
683
683
  "--experimental",
684
+ "experiments",
684
685
  help="Enable experimental support for specific features.",
685
- type=click.Choice([experimental.OPEN_API_3_1.name]),
686
+ type=click.Choice([experimental.OPEN_API_3_1.name, experimental.SCHEMA_ANALYSIS.name]),
686
687
  callback=callbacks.convert_experimental,
687
688
  multiple=True,
688
689
  )
@@ -737,7 +738,7 @@ def run(
737
738
  set_header: dict[str, str],
738
739
  set_cookie: dict[str, str],
739
740
  set_path: dict[str, str],
740
- experimental: list,
741
+ experiments: list,
741
742
  checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
742
743
  exclude_checks: Iterable[str] = (),
743
744
  data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
@@ -826,7 +827,7 @@ def run(
826
827
  show_trace = show_errors_tracebacks
827
828
 
828
829
  # Enable selected experiments
829
- for experiment in experimental:
830
+ for experiment in experiments:
830
831
  experiment.enable()
831
832
 
832
833
  override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
@@ -901,6 +902,10 @@ def run(
901
902
  from ..service.client import ServiceClient
902
903
 
903
904
  # Upload without connecting data to a certain API
905
+ client = ServiceClient(base_url=schemathesis_io_url, token=token)
906
+ if experimental.SCHEMA_ANALYSIS.is_enabled and not client:
907
+ from ..service.client import ServiceClient
908
+
904
909
  client = ServiceClient(base_url=schemathesis_io_url, token=token)
905
910
  host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
906
911
 
@@ -970,6 +975,7 @@ def run(
970
975
  stateful_recursion_limit=stateful_recursion_limit,
971
976
  hypothesis_settings=hypothesis_settings,
972
977
  generation_config=generation_config,
978
+ service_client=client,
973
979
  )
974
980
  execute(
975
981
  event_stream,
@@ -1074,6 +1080,7 @@ def into_event_stream(
1074
1080
  store_interactions: bool,
1075
1081
  stateful: Stateful | None,
1076
1082
  stateful_recursion_limit: int,
1083
+ service_client: ServiceClient | None,
1077
1084
  ) -> Generator[events.ExecutionEvent, None, None]:
1078
1085
  try:
1079
1086
  if app is not None:
@@ -1133,6 +1140,7 @@ def into_event_stream(
1133
1140
  auth_type=config.auth_type,
1134
1141
  headers=config.headers,
1135
1142
  ),
1143
+ service_client=service_client,
1136
1144
  ).execute()
1137
1145
  except SchemaError as error:
1138
1146
  yield events.InternalError.from_schema_error(error)
@@ -254,8 +254,8 @@ def write_double_quoted(stream: IO, text: str) -> None:
254
254
  ch = text[end]
255
255
  if (
256
256
  ch is None
257
- or ch in '"\\\x85\u2028\u2029\uFEFF'
258
- or not ("\x20" <= ch <= "\x7E" or ("\xA0" <= ch <= "\uD7FF" or "\uE000" <= ch <= "\uFFFD"))
257
+ or ch in '"\\\x85\u2028\u2029\ufeff'
258
+ or not ("\x20" <= ch <= "\x7e" or ("\xa0" <= ch <= "\ud7ff" or "\ue000" <= ch <= "\ufffd"))
259
259
  ):
260
260
  if start < end:
261
261
  stream.write(text[start:end])
@@ -264,9 +264,9 @@ def write_double_quoted(stream: IO, text: str) -> None:
264
264
  # Escape character
265
265
  if ch in Emitter.ESCAPE_REPLACEMENTS:
266
266
  data = "\\" + Emitter.ESCAPE_REPLACEMENTS[ch]
267
- elif ch <= "\xFF":
267
+ elif ch <= "\xff":
268
268
  data = "\\x%02X" % ord(ch)
269
- elif ch <= "\uFFFF":
269
+ elif ch <= "\uffff":
270
270
  data = "\\u%04X" % ord(ch)
271
271
  else:
272
272
  data = "\\U%08X" % ord(ch)
@@ -8,8 +8,10 @@ from typing import TYPE_CHECKING
8
8
 
9
9
  from ..code_samples import CodeSampleStyle
10
10
  from ..internal.deprecation import deprecated_property
11
- from ..runner.serialization import SerializedTestResult
11
+ from ..internal.result import Result
12
12
  from ..runner.probes import ProbeRun
13
+ from ..runner.serialization import SerializedTestResult
14
+ from ..service.models import AnalysisResult
13
15
 
14
16
  if TYPE_CHECKING:
15
17
  import hypothesis
@@ -52,6 +54,7 @@ class ExecutionContext:
52
54
  code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
53
55
  report: ServiceReportContext | FileReportContext | None = None
54
56
  probes: list[ProbeRun] | None = None
57
+ analysis: Result[AnalysisResult, Exception] | None = None
55
58
 
56
59
  @deprecated_property(removed_in="4.0", replacement="show_trace")
57
60
  def show_errors_tracebacks(self) -> bool:
@@ -9,7 +9,7 @@ import time
9
9
  from importlib import metadata
10
10
  from itertools import groupby
11
11
  from queue import Queue
12
- from typing import Any, Generator, cast
12
+ from typing import Any, Generator, cast, TYPE_CHECKING
13
13
 
14
14
  import click
15
15
 
@@ -25,16 +25,26 @@ from ...constants import (
25
25
  SCHEMATHESIS_TEST_CASE_HEADER,
26
26
  SCHEMATHESIS_VERSION,
27
27
  )
28
- from ...exceptions import RuntimeErrorType, prepare_response_payload
28
+ from ...exceptions import (
29
+ RuntimeErrorType,
30
+ format_exception,
31
+ prepare_response_payload,
32
+ extract_requests_exception_details,
33
+ )
29
34
  from ...experimental import GLOBAL_EXPERIMENTS
35
+ from ...internal.result import Ok
30
36
  from ...models import Status
31
37
  from ...runner import events
32
38
  from ...runner.events import InternalErrorType, SchemaErrorType
33
39
  from ...runner.probes import ProbeOutcome
34
40
  from ...runner.serialization import SerializedCheck, SerializedError, SerializedTestResult, deduplicate_failures
41
+ from ...service.models import AnalysisSuccess, UnknownExtension, ErrorState
35
42
  from ..context import ExecutionContext, FileReportContext, ServiceReportContext
36
43
  from ..handlers import EventHandler
37
44
 
45
+ if TYPE_CHECKING:
46
+ import requests
47
+
38
48
  SPINNER_REPETITION_NUMBER = 10
39
49
 
40
50
 
@@ -181,16 +191,19 @@ def display_generic_errors(context: ExecutionContext, errors: list[SerializedErr
181
191
 
182
192
  def display_full_traceback_message(error: SerializedError) -> bool:
183
193
  # Some errors should not trigger the message that suggests to show full tracebacks to the user
184
- return not error.exception.startswith(
185
- (
186
- "DeadlineExceeded",
187
- "OperationSchemaError",
188
- "requests.exceptions",
189
- "SerializationNotPossible",
190
- "hypothesis.errors.FailedHealthCheck",
191
- "hypothesis.errors.InvalidArgument: Scalar ",
192
- "hypothesis.errors.InvalidArgument: min_size=",
194
+ return (
195
+ not error.exception.startswith(
196
+ (
197
+ "DeadlineExceeded",
198
+ "OperationSchemaError",
199
+ "requests.exceptions",
200
+ "SerializationNotPossible",
201
+ "hypothesis.errors.FailedHealthCheck",
202
+ "hypothesis.errors.InvalidArgument: Scalar ",
203
+ "hypothesis.errors.InvalidArgument: min_size=",
204
+ )
193
205
  )
206
+ and "can never generate an example, because min_size is larger than Hypothesis supports." not in error.exception
194
207
  )
195
208
 
196
209
 
@@ -359,6 +372,81 @@ def display_single_log(result: SerializedTestResult) -> None:
359
372
  click.echo("\n\n".join(result.logs))
360
373
 
361
374
 
375
+ def display_analysis(context: ExecutionContext) -> None:
376
+ """Display schema analysis details."""
377
+ import requests.exceptions
378
+
379
+ if context.analysis is None:
380
+ return
381
+ display_section_name("SCHEMA ANALYSIS")
382
+ if isinstance(context.analysis, Ok):
383
+ analysis = context.analysis.ok()
384
+ click.echo()
385
+ if isinstance(analysis, AnalysisSuccess):
386
+ click.secho(analysis.message, bold=True)
387
+ click.echo("\nAnalysis took: {:.2f}ms".format(analysis.elapsed))
388
+ if analysis.extensions:
389
+ known = []
390
+ failed = []
391
+ unknown = []
392
+ for extension in analysis.extensions:
393
+ if isinstance(extension, UnknownExtension):
394
+ unknown.append(extension)
395
+ elif isinstance(extension.state, ErrorState):
396
+ failed.append(extension)
397
+ else:
398
+ known.append(extension)
399
+ if known:
400
+ click.echo("\nThe following extensions have been applied:\n")
401
+ for extension in known:
402
+ click.echo(f" - {extension.summary}")
403
+ if failed:
404
+ click.echo("\nThe following extensions errored:\n")
405
+ for extension in failed:
406
+ click.echo(f" - {extension.summary}")
407
+ suggestion = f"Please, consider reporting this to our issue tracker:\n\n {ISSUE_TRACKER_URL}"
408
+ click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
409
+ if unknown:
410
+ noun = "extension" if len(unknown) == 1 else "extensions"
411
+ specific_noun = "this extension" if len(unknown) == 1 else "these extensions"
412
+ title = click.style("Compatibility Notice", bold=True)
413
+ click.secho(f"\n{title}: {len(unknown)} {noun} not recognized:\n")
414
+ for extension in unknown:
415
+ click.echo(f" - {extension.summary}")
416
+ suggestion = f"Consider updating the CLI to add support for {specific_noun}."
417
+ click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
418
+ else:
419
+ click.echo("\nNo extensions have been applied.")
420
+ else:
421
+ click.echo("An error happened during schema analysis:\n")
422
+ click.secho(f" {analysis.message}", bold=True)
423
+ click.echo()
424
+ else:
425
+ exception = context.analysis.err()
426
+ suggestion = None
427
+ if isinstance(exception, requests.exceptions.HTTPError):
428
+ response = exception.response
429
+ click.secho("Error\n", fg="red", bold=True)
430
+ _display_service_network_error(response)
431
+ click.echo()
432
+ return None
433
+ elif isinstance(exception, requests.RequestException):
434
+ message, extras = extract_requests_exception_details(exception)
435
+ suggestion = "Please check your network connection and try again."
436
+ title = "Network Error"
437
+ else:
438
+ traceback = format_exception(exception, True)
439
+ extras = _split_traceback(traceback)
440
+ title = "Internal Error"
441
+ message = f"We apologize for the inconvenience. This appears to be an internal issue.\nPlease, consider reporting the following details to our issue tracker:\n\n {ISSUE_TRACKER_URL}"
442
+ suggestion = "Please update your CLI to the latest version and try again."
443
+ click.secho(f"{title}\n", fg="red", bold=True)
444
+ click.echo(message)
445
+ _display_extras(extras)
446
+ _maybe_display_tip(suggestion)
447
+ click.echo()
448
+
449
+
362
450
  def display_statistic(context: ExecutionContext, event: events.Finished) -> None:
363
451
  """Format and print statistic collected by :obj:`models.TestResult`."""
364
452
  display_section_name("SUMMARY")
@@ -493,39 +581,45 @@ def display_service_error(event: service.Error, message_prefix: str = "") -> Non
493
581
 
494
582
  if isinstance(event.exception, HTTPError):
495
583
  response = cast(Response, event.exception.response)
496
- status_code = response.status_code
497
- if 500 <= status_code <= 599:
498
- click.secho(f"Schemathesis.io responded with HTTP {status_code}", fg="red")
499
- # Server error, should be resolved soon
500
- click.secho(
501
- "\nIt is likely that we are already notified about the issue and working on a fix\n"
502
- "Please, try again in 30 minutes",
503
- fg="red",
504
- )
505
- elif status_code == 401:
506
- # Likely an invalid token
507
- click.echo("Your CLI is not authenticated.")
508
- display_service_unauthorized("schemathesis.io")
509
- else:
510
- try:
511
- data = response.json()
512
- detail = data["detail"]
513
- click.secho(f"{message_prefix}{detail}", fg="red")
514
- except Exception:
515
- # Other client-side errors are likely caused by a bug on the CLI side
516
- click.secho(
517
- "We apologize for the inconvenience. This appears to be an internal issue.\n"
518
- "Please, consider reporting the following details to our issue "
519
- f"tracker:\n\n {ISSUE_TRACKER_URL}\n\nResponse: {response.text!r}\n"
520
- f"Headers: {response.headers!r}",
521
- fg="red",
522
- )
584
+ _display_service_network_error(response, message_prefix)
523
585
  elif isinstance(event.exception, RequestException):
524
586
  ask_to_report(event, report_to_issues=False)
525
587
  else:
526
588
  ask_to_report(event)
527
589
 
528
590
 
591
+ def _display_service_network_error(response: requests.Response, message_prefix: str = "") -> None:
592
+ status_code = response.status_code
593
+ if 500 <= status_code <= 599:
594
+ click.secho(f"Schemathesis.io responded with HTTP {status_code}", fg="red")
595
+ # Server error, should be resolved soon
596
+ click.secho(
597
+ "\nIt is likely that we are already notified about the issue and working on a fix\n"
598
+ "Please, try again in 30 minutes",
599
+ fg="red",
600
+ )
601
+ elif status_code == 401:
602
+ # Likely an invalid token
603
+ click.echo("Your CLI is not authenticated.")
604
+ display_service_unauthorized("schemathesis.io")
605
+ else:
606
+ try:
607
+ data = response.json()
608
+ detail = data["detail"]
609
+ click.secho(f"{message_prefix}{detail}", fg="red")
610
+ except Exception:
611
+ # Other client-side errors are likely caused by a bug on the CLI side
612
+ click.secho(
613
+ "We apologize for the inconvenience. This appears to be an internal issue.\n"
614
+ "Please, consider reporting the following details to our issue "
615
+ f"tracker:\n\n {ISSUE_TRACKER_URL}\n\nResponse: {response.text!r}\n"
616
+ f"Status: {response.status_code}\n"
617
+ f"Headers: {response.headers!r}",
618
+ fg="red",
619
+ )
620
+ _maybe_display_tip("Please update your CLI to the latest version and try again.")
621
+
622
+
529
623
  SERVICE_ERROR_MESSAGE = "An error happened during uploading reports to Schemathesis.io"
530
624
 
531
625
 
@@ -712,7 +806,23 @@ def handle_after_probing(context: ExecutionContext, event: events.AfterProbing)
712
806
  status = "SUCCESS"
713
807
  elif probe.outcome == ProbeOutcome.ERROR:
714
808
  status = "ERROR"
715
- click.secho(f"API probing: {status}\r", bold=True, nl=False)
809
+ click.secho(f"API probing: {status}", bold=True, nl=False)
810
+ click.echo()
811
+
812
+
813
+ def handle_before_analysis(context: ExecutionContext, event: events.BeforeAnalysis) -> None:
814
+ click.secho("Schema analysis: ...\r", bold=True, nl=False)
815
+
816
+
817
+ def handle_after_analysis(context: ExecutionContext, event: events.AfterAnalysis) -> None:
818
+ context.analysis = event.analysis
819
+ status = "SKIP"
820
+ if event.analysis is not None:
821
+ if isinstance(event.analysis, Ok) and isinstance(event.analysis.ok(), AnalysisSuccess):
822
+ status = "SUCCESS"
823
+ else:
824
+ status = "ERROR"
825
+ click.secho(f"Schema analysis: {status}", bold=True, nl=False)
716
826
  click.echo()
717
827
  operations_count = cast(int, context.operations_count) # INVARIANT: should not be `None`
718
828
  if operations_count >= 1:
@@ -752,6 +862,7 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
752
862
  display_errors(context, event)
753
863
  display_failures(context, event)
754
864
  display_application_logs(context, event)
865
+ display_analysis(context)
755
866
  display_statistic(context, event)
756
867
  click.echo()
757
868
  display_summary(event)
@@ -777,6 +888,10 @@ class DefaultOutputStyleHandler(EventHandler):
777
888
  handle_before_probing(context, event)
778
889
  if isinstance(event, events.AfterProbing):
779
890
  handle_after_probing(context, event)
891
+ if isinstance(event, events.BeforeAnalysis):
892
+ handle_before_analysis(context, event)
893
+ if isinstance(event, events.AfterAnalysis):
894
+ handle_after_analysis(context, event)
780
895
  if isinstance(event, events.BeforeExecution):
781
896
  handle_before_execution(context, event)
782
897
  if isinstance(event, events.AfterExecution):
@@ -30,6 +30,10 @@ class ShortOutputStyleHandler(EventHandler):
30
30
  default.handle_before_probing(context, event)
31
31
  if isinstance(event, events.AfterProbing):
32
32
  default.handle_after_probing(context, event)
33
+ if isinstance(event, events.BeforeAnalysis):
34
+ default.handle_before_analysis(context, event)
35
+ if isinstance(event, events.AfterAnalysis):
36
+ default.handle_after_analysis(context, event)
33
37
  if isinstance(event, events.BeforeExecution):
34
38
  handle_before_execution(context, event)
35
39
  if isinstance(event, events.AfterExecution):
@@ -72,3 +72,10 @@ OPEN_API_3_1 = GLOBAL_EXPERIMENTS.create_experiment(
72
72
  description="Support for response validation",
73
73
  discussion_url="https://github.com/schemathesis/schemathesis/discussions/1822",
74
74
  )
75
+ SCHEMA_ANALYSIS = GLOBAL_EXPERIMENTS.create_experiment(
76
+ name="schema-analysis",
77
+ verbose_name="Schema Analysis",
78
+ env_var="SCHEMA_ANALYSIS",
79
+ description="Analyzing API schemas via Schemathesis.io",
80
+ discussion_url="https://github.com/schemathesis/schemathesis/discussions/2056",
81
+ )
schemathesis/filters.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """Filtering system that allows users to filter API operations based on certain criteria."""
2
+
2
3
  from __future__ import annotations
3
4
  import re
4
5
  from dataclasses import dataclass, field
schemathesis/models.py CHANGED
@@ -125,6 +125,8 @@ class Case:
125
125
  """A single test case parameters."""
126
126
 
127
127
  operation: APIOperation
128
+ # Time spent on generation of this test case
129
+ generation_time: float
128
130
  # Unique test case identifier
129
131
  id: str = field(default_factory=generate_random_case_id, compare=False)
130
132
  path_parameters: PathParameters | None = None
@@ -579,6 +581,7 @@ class Case:
579
581
  cookies=fast_deepcopy(self.cookies),
580
582
  query=fast_deepcopy(self.query),
581
583
  body=fast_deepcopy(self.body),
584
+ generation_time=self.generation_time,
582
585
  )
583
586
 
584
587
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  These are basic entities that describe what data could be sent to the API.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
  from dataclasses import dataclass, field
7
8
  from typing import TYPE_CHECKING, Any, Generator, Generic, TypeVar
@@ -31,6 +31,7 @@ if TYPE_CHECKING:
31
31
  from ..stateful import Stateful
32
32
  from . import events
33
33
  from .impl import BaseRunner
34
+ from ..service.client import ServiceClient
34
35
 
35
36
 
36
37
  @deprecated_function(removed_in="4.0", replacement="schemathesis.runner.from_schema")
@@ -78,6 +79,7 @@ def prepare(
78
79
  hypothesis_suppress_health_check: list[hypothesis.HealthCheck] | None = None,
79
80
  hypothesis_verbosity: hypothesis.Verbosity | None = None,
80
81
  probe_config: ProbeConfig | None = None,
82
+ service_client: ServiceClient | None = None,
81
83
  ) -> Generator[events.ExecutionEvent, None, None]:
82
84
  """Prepare a generator that will run test cases against the given API definition."""
83
85
  from ..checks import DEFAULT_CHECKS
@@ -132,6 +134,7 @@ def prepare(
132
134
  count_operations=count_operations,
133
135
  count_links=count_links,
134
136
  probe_config=probe_config,
137
+ service_client=service_client,
135
138
  )
136
139
 
137
140
 
@@ -193,6 +196,7 @@ def execute_from_schema(
193
196
  count_operations: bool = True,
194
197
  count_links: bool = True,
195
198
  probe_config: ProbeConfig | None = None,
199
+ service_client: ServiceClient | None,
196
200
  ) -> Generator[events.ExecutionEvent, None, None]:
197
201
  """Execute tests for the given schema.
198
202
 
@@ -243,6 +247,7 @@ def execute_from_schema(
243
247
  count_operations=count_operations,
244
248
  count_links=count_links,
245
249
  probe_config=probe_config,
250
+ service_client=service_client,
246
251
  ).execute()
247
252
  except SchemaError as error:
248
253
  yield events.InternalError.from_schema_error(error)
@@ -348,6 +353,7 @@ def from_schema(
348
353
  count_operations: bool = True,
349
354
  count_links: bool = True,
350
355
  probe_config: ProbeConfig | None = None,
356
+ service_client: ServiceClient | None = None,
351
357
  ) -> BaseRunner:
352
358
  import hypothesis
353
359
  from starlette.applications import Starlette
@@ -402,6 +408,7 @@ def from_schema(
402
408
  count_operations=count_operations,
403
409
  count_links=count_links,
404
410
  probe_config=probe_config,
411
+ service_client=service_client,
405
412
  )
406
413
  if isinstance(schema.app, Starlette):
407
414
  return ThreadPoolASGIRunner(
@@ -426,6 +433,7 @@ def from_schema(
426
433
  count_operations=count_operations,
427
434
  count_links=count_links,
428
435
  probe_config=probe_config,
436
+ service_client=service_client,
429
437
  )
430
438
  return ThreadPoolWSGIRunner(
431
439
  schema=schema,
@@ -450,6 +458,7 @@ def from_schema(
450
458
  count_operations=count_operations,
451
459
  count_links=count_links,
452
460
  probe_config=probe_config,
461
+ service_client=service_client,
453
462
  )
454
463
  if not schema.app:
455
464
  return SingleThreadRunner(
@@ -478,6 +487,7 @@ def from_schema(
478
487
  count_operations=count_operations,
479
488
  count_links=count_links,
480
489
  probe_config=probe_config,
490
+ service_client=service_client,
481
491
  )
482
492
  if isinstance(schema.app, Starlette):
483
493
  return SingleThreadASGIRunner(
@@ -502,6 +512,7 @@ def from_schema(
502
512
  count_operations=count_operations,
503
513
  count_links=count_links,
504
514
  probe_config=probe_config,
515
+ service_client=service_client,
505
516
  )
506
517
  return SingleThreadWSGIRunner(
507
518
  schema=schema,
@@ -525,6 +536,7 @@ def from_schema(
525
536
  count_operations=count_operations,
526
537
  count_links=count_links,
527
538
  probe_config=probe_config,
539
+ service_client=service_client,
528
540
  )
529
541
 
530
542
 
@@ -6,6 +6,7 @@ from dataclasses import asdict, dataclass, field
6
6
  from typing import Any, TYPE_CHECKING
7
7
 
8
8
  from ..internal.datetime import current_datetime
9
+ from ..internal.result import Result
9
10
  from ..generation import DataGenerationMethod
10
11
  from ..exceptions import SchemaError, SchemaErrorType, format_exception, RuntimeErrorType
11
12
  from .serialization import SerializedError, SerializedTestResult
@@ -14,6 +15,7 @@ from .serialization import SerializedError, SerializedTestResult
14
15
  if TYPE_CHECKING:
15
16
  from ..models import APIOperation, Status, TestResult, TestResultSet
16
17
  from ..schemas import BaseSchema
18
+ from ..service.models import AnalysisResult
17
19
  from . import probes
18
20
 
19
21
 
@@ -93,6 +95,16 @@ class AfterProbing(ExecutionEvent):
93
95
  return {"probes": [probe.serialize() for probe in probes], "events_type": self.__class__.__name__}
94
96
 
95
97
 
98
+ @dataclass
99
+ class BeforeAnalysis(ExecutionEvent):
100
+ pass
101
+
102
+
103
+ @dataclass
104
+ class AfterAnalysis(ExecutionEvent):
105
+ analysis: Result[AnalysisResult, Exception] | None
106
+
107
+
96
108
  class CurrentOperationMixin:
97
109
  method: str
98
110
  path: str