schemathesis 3.25.5__py3-none-any.whl → 3.26.0__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 +1 -0
  3. schemathesis/_xml.py +1 -0
  4. schemathesis/auths.py +1 -0
  5. schemathesis/cli/__init__.py +39 -37
  6. schemathesis/cli/cassettes.py +4 -4
  7. schemathesis/cli/context.py +6 -0
  8. schemathesis/cli/output/default.py +185 -45
  9. schemathesis/cli/output/short.py +8 -0
  10. schemathesis/experimental/__init__.py +7 -0
  11. schemathesis/filters.py +1 -0
  12. schemathesis/models.py +5 -2
  13. schemathesis/parameters.py +1 -0
  14. schemathesis/runner/__init__.py +36 -9
  15. schemathesis/runner/events.py +33 -1
  16. schemathesis/runner/impl/core.py +99 -23
  17. schemathesis/{cli → runner}/probes.py +32 -21
  18. schemathesis/runner/serialization.py +4 -2
  19. schemathesis/schemas.py +1 -0
  20. schemathesis/serializers.py +11 -3
  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 +44 -1
  27. schemathesis/specs/openapi/__init__.py +1 -0
  28. schemathesis/specs/openapi/_hypothesis.py +9 -1
  29. schemathesis/specs/openapi/examples.py +22 -24
  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 +10 -3
  37. schemathesis/specs/openapi/security.py +5 -1
  38. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/METADATA +8 -5
  39. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/RECORD +42 -40
  40. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/WHEEL +0 -0
  41. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.dist-info}/entry_points.txt +0 -0
  42. {schemathesis-3.25.5.dist-info → schemathesis-3.26.0.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
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,48 +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
- from ..runner import events, prepare_hypothesis_settings
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
- from ..specs.openapi import formats
48
47
  from ..stateful import Stateful
49
48
  from ..targets import Target
49
+ from ..transports.auth import get_requests_auth
50
50
  from ..types import Filter, PathLike, RequestCert
51
- from ..internal.datetime import current_datetime
52
- from ..internal.validation import file_exists
53
- from . import callbacks, cassettes, output, probes
54
- from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS
51
+ from . import callbacks, cassettes, output
52
+ from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
55
53
  from .context import ExecutionContext, FileReportContext, ServiceReportContext
56
54
  from .debug import DebugOutputHandler
57
55
  from .junitxml import JunitXMLHandler
@@ -61,8 +59,9 @@ from .sanitization import SanitizationHandler
61
59
  if TYPE_CHECKING:
62
60
  import hypothesis
63
61
  import requests
64
- from ..service.client import ServiceClient
62
+
65
63
  from ..schemas import BaseSchema
64
+ from ..service.client import ServiceClient
66
65
  from ..specs.graphql.schemas import GraphQLSchema
67
66
  from .handlers import EventHandler
68
67
 
@@ -682,8 +681,9 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
682
681
  @click.option("--force-color", help="Explicitly tells to enable ANSI color escape codes.", type=bool, is_flag=True)
683
682
  @click.option(
684
683
  "--experimental",
684
+ "experiments",
685
685
  help="Enable experimental support for specific features.",
686
- type=click.Choice([experimental.OPEN_API_3_1.name]),
686
+ type=click.Choice([experimental.OPEN_API_3_1.name, experimental.SCHEMA_ANALYSIS.name]),
687
687
  callback=callbacks.convert_experimental,
688
688
  multiple=True,
689
689
  )
@@ -738,7 +738,7 @@ def run(
738
738
  set_header: dict[str, str],
739
739
  set_cookie: dict[str, str],
740
740
  set_path: dict[str, str],
741
- experimental: list,
741
+ experiments: list,
742
742
  checks: Iterable[str] = DEFAULT_CHECKS_NAMES,
743
743
  exclude_checks: Iterable[str] = (),
744
744
  data_generation_methods: tuple[DataGenerationMethod, ...] = DEFAULT_DATA_GENERATION_METHODS,
@@ -827,7 +827,7 @@ def run(
827
827
  show_trace = show_errors_tracebacks
828
828
 
829
829
  # Enable selected experiments
830
- for experiment in experimental:
830
+ for experiment in experiments:
831
831
  experiment.enable()
832
832
 
833
833
  override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
@@ -902,6 +902,10 @@ def run(
902
902
  from ..service.client import ServiceClient
903
903
 
904
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
+
905
909
  client = ServiceClient(base_url=schemathesis_io_url, token=token)
906
910
  host_data = service.hosts.HostData(schemathesis_io_hostname, hosts_file)
907
911
 
@@ -971,6 +975,7 @@ def run(
971
975
  stateful_recursion_limit=stateful_recursion_limit,
972
976
  hypothesis_settings=hypothesis_settings,
973
977
  generation_config=generation_config,
978
+ service_client=client,
974
979
  )
975
980
  execute(
976
981
  event_stream,
@@ -1075,6 +1080,7 @@ def into_event_stream(
1075
1080
  store_interactions: bool,
1076
1081
  stateful: Stateful | None,
1077
1082
  stateful_recursion_limit: int,
1083
+ service_client: ServiceClient | None,
1078
1084
  ) -> Generator[events.ExecutionEvent, None, None]:
1079
1085
  try:
1080
1086
  if app is not None:
@@ -1100,10 +1106,9 @@ def into_event_stream(
1100
1106
  tag=tag or None,
1101
1107
  operation_id=operation_id or None,
1102
1108
  )
1103
- loaded_schema = load_schema(config)
1104
- run_probes(loaded_schema, config)
1109
+ schema = load_schema(config)
1105
1110
  yield from runner.from_schema(
1106
- loaded_schema,
1111
+ schema,
1107
1112
  auth=auth,
1108
1113
  auth_type=auth_type,
1109
1114
  override=override,
@@ -1126,6 +1131,16 @@ def into_event_stream(
1126
1131
  stateful_recursion_limit=stateful_recursion_limit,
1127
1132
  hypothesis_settings=hypothesis_settings,
1128
1133
  generation_config=generation_config,
1134
+ probe_config=probes.ProbeConfig(
1135
+ base_url=config.base_url,
1136
+ request_tls_verify=config.request_tls_verify,
1137
+ request_proxy=config.request_proxy,
1138
+ request_cert=config.request_cert,
1139
+ auth=config.auth,
1140
+ auth_type=config.auth_type,
1141
+ headers=config.headers,
1142
+ ),
1143
+ service_client=service_client,
1129
1144
  ).execute()
1130
1145
  except SchemaError as error:
1131
1146
  yield events.InternalError.from_schema_error(error)
@@ -1133,19 +1148,6 @@ def into_event_stream(
1133
1148
  yield events.InternalError.from_exc(exc)
1134
1149
 
1135
1150
 
1136
- def run_probes(schema: BaseSchema, config: LoaderConfig) -> None:
1137
- """Discover capabilities of the tested app."""
1138
- probe_results = probes.run(schema, config)
1139
- for result in probe_results:
1140
- if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
1141
- from ..specs.openapi._hypothesis import HEADER_FORMAT, header_values
1142
-
1143
- formats.register(
1144
- HEADER_FORMAT,
1145
- header_values(blacklist_characters="\n\r\x00").map(str.lstrip),
1146
- )
1147
-
1148
-
1149
1151
  def load_schema(config: LoaderConfig) -> BaseSchema:
1150
1152
  """Automatically load API schema."""
1151
1153
  first: Callable[[LoaderConfig], BaseSchema]
@@ -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)
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import os
3
4
  import shutil
4
5
  from dataclasses import dataclass, field
@@ -7,7 +8,10 @@ from typing import TYPE_CHECKING
7
8
 
8
9
  from ..code_samples import CodeSampleStyle
9
10
  from ..internal.deprecation import deprecated_property
11
+ from ..internal.result import Result
12
+ from ..runner.probes import ProbeRun
10
13
  from ..runner.serialization import SerializedTestResult
14
+ from ..service.models import AnalysisResult
11
15
 
12
16
  if TYPE_CHECKING:
13
17
  import hypothesis
@@ -49,6 +53,8 @@ class ExecutionContext:
49
53
  verbosity: int = 0
50
54
  code_sample_style: CodeSampleStyle = CodeSampleStyle.default()
51
55
  report: ServiceReportContext | FileReportContext | None = None
56
+ probes: list[ProbeRun] | None = None
57
+ analysis: Result[AnalysisResult, Exception] | None = None
52
58
 
53
59
  @deprecated_property(removed_in="4.0", replacement="show_trace")
54
60
  def show_errors_tracebacks(self) -> bool:
@@ -1,38 +1,50 @@
1
1
  from __future__ import annotations
2
+
2
3
  import base64
3
4
  import os
4
5
  import platform
5
6
  import shutil
6
7
  import textwrap
7
8
  import time
9
+ from importlib import metadata
8
10
  from itertools import groupby
9
11
  from queue import Queue
10
- from typing import Any, Generator, cast
12
+ from typing import Any, Generator, cast, TYPE_CHECKING
11
13
 
12
14
  import click
13
- from importlib import metadata
14
15
 
15
16
  from ... import service
16
17
  from ...code_samples import CodeSampleStyle
17
18
  from ...constants import (
18
19
  DISCORD_LINK,
20
+ FALSE_VALUES,
19
21
  FLAKY_FAILURE_MESSAGE,
22
+ GITHUB_APP_LINK,
23
+ ISSUE_TRACKER_URL,
20
24
  REPORT_SUGGESTION_ENV_VAR,
21
25
  SCHEMATHESIS_TEST_CASE_HEADER,
22
26
  SCHEMATHESIS_VERSION,
23
- FALSE_VALUES,
24
- ISSUE_TRACKER_URL,
25
- GITHUB_APP_LINK,
26
27
  )
27
- 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
+ )
28
34
  from ...experimental import GLOBAL_EXPERIMENTS
35
+ from ...internal.result import Ok
29
36
  from ...models import Status
30
37
  from ...runner import events
31
38
  from ...runner.events import InternalErrorType, SchemaErrorType
32
- from ...runner.serialization import SerializedError, SerializedTestResult, deduplicate_failures, SerializedCheck
39
+ from ...runner.probes import ProbeOutcome
40
+ from ...runner.serialization import SerializedCheck, SerializedError, SerializedTestResult, deduplicate_failures
41
+ from ...service.models import AnalysisSuccess, UnknownExtension, ErrorState
33
42
  from ..context import ExecutionContext, FileReportContext, ServiceReportContext
34
43
  from ..handlers import EventHandler
35
44
 
45
+ if TYPE_CHECKING:
46
+ import requests
47
+
36
48
  SPINNER_REPETITION_NUMBER = 10
37
49
 
38
50
 
@@ -179,16 +191,19 @@ def display_generic_errors(context: ExecutionContext, errors: list[SerializedErr
179
191
 
180
192
  def display_full_traceback_message(error: SerializedError) -> bool:
181
193
  # Some errors should not trigger the message that suggests to show full tracebacks to the user
182
- return not error.exception.startswith(
183
- (
184
- "DeadlineExceeded",
185
- "OperationSchemaError",
186
- "requests.exceptions",
187
- "SerializationNotPossible",
188
- "hypothesis.errors.FailedHealthCheck",
189
- "hypothesis.errors.InvalidArgument: Scalar ",
190
- "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
+ )
191
205
  )
206
+ and "can never generate an example, because min_size is larger than Hypothesis supports." not in error.exception
192
207
  )
193
208
 
194
209
 
@@ -357,6 +372,81 @@ def display_single_log(result: SerializedTestResult) -> None:
357
372
  click.echo("\n\n".join(result.logs))
358
373
 
359
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
+
360
450
  def display_statistic(context: ExecutionContext, event: events.Finished) -> None:
361
451
  """Format and print statistic collected by :obj:`models.TestResult`."""
362
452
  display_section_name("SUMMARY")
@@ -487,43 +577,49 @@ def display_service_unauthorized(hostname: str) -> None:
487
577
 
488
578
  def display_service_error(event: service.Error, message_prefix: str = "") -> None:
489
579
  """Show information about an error during communication with Schemathesis.io."""
490
- from requests import RequestException, HTTPError, Response
580
+ from requests import HTTPError, RequestException, Response
491
581
 
492
582
  if isinstance(event.exception, HTTPError):
493
583
  response = cast(Response, event.exception.response)
494
- status_code = response.status_code
495
- if 500 <= status_code <= 599:
496
- click.secho(f"Schemathesis.io responded with HTTP {status_code}", fg="red")
497
- # Server error, should be resolved soon
498
- click.secho(
499
- "\nIt is likely that we are already notified about the issue and working on a fix\n"
500
- "Please, try again in 30 minutes",
501
- fg="red",
502
- )
503
- elif status_code == 401:
504
- # Likely an invalid token
505
- click.echo("Your CLI is not authenticated.")
506
- display_service_unauthorized("schemathesis.io")
507
- else:
508
- try:
509
- data = response.json()
510
- detail = data["detail"]
511
- click.secho(f"{message_prefix}{detail}", fg="red")
512
- except Exception:
513
- # Other client-side errors are likely caused by a bug on the CLI side
514
- click.secho(
515
- "We apologize for the inconvenience. This appears to be an internal issue.\n"
516
- "Please, consider reporting the following details to our issue "
517
- f"tracker:\n\n {ISSUE_TRACKER_URL}\n\nResponse: {response.text!r}\n"
518
- f"Headers: {response.headers!r}",
519
- fg="red",
520
- )
584
+ _display_service_network_error(response, message_prefix)
521
585
  elif isinstance(event.exception, RequestException):
522
586
  ask_to_report(event, report_to_issues=False)
523
587
  else:
524
588
  ask_to_report(event)
525
589
 
526
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
+
527
623
  SERVICE_ERROR_MESSAGE = "An error happened during uploading reports to Schemathesis.io"
528
624
 
529
625
 
@@ -694,7 +790,42 @@ def handle_initialized(context: ExecutionContext, event: events.Initialized) ->
694
790
  click.secho(f"Collected API links: {links_count}", bold=True)
695
791
  if isinstance(context.report, ServiceReportContext):
696
792
  click.secho("Report to Schemathesis.io: ENABLED", bold=True)
697
- if context.operations_count >= 1:
793
+
794
+
795
+ def handle_before_probing(context: ExecutionContext, event: events.BeforeProbing) -> None:
796
+ click.secho("API probing: ...\r", bold=True, nl=False)
797
+
798
+
799
+ def handle_after_probing(context: ExecutionContext, event: events.AfterProbing) -> None:
800
+ context.probes = event.probes
801
+ status = "SKIP"
802
+ if event.probes is not None:
803
+ for probe in event.probes:
804
+ if probe.outcome in (ProbeOutcome.SUCCESS, ProbeOutcome.FAILURE):
805
+ # The probe itself has been executed
806
+ status = "SUCCESS"
807
+ elif probe.outcome == ProbeOutcome.ERROR:
808
+ status = "ERROR"
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)
826
+ click.echo()
827
+ operations_count = cast(int, context.operations_count) # INVARIANT: should not be `None`
828
+ if operations_count >= 1:
698
829
  click.echo()
699
830
 
700
831
 
@@ -731,6 +862,7 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
731
862
  display_errors(context, event)
732
863
  display_failures(context, event)
733
864
  display_application_logs(context, event)
865
+ display_analysis(context)
734
866
  display_statistic(context, event)
735
867
  click.echo()
736
868
  display_summary(event)
@@ -752,6 +884,14 @@ class DefaultOutputStyleHandler(EventHandler):
752
884
  """Choose and execute a proper handler for the given event."""
753
885
  if isinstance(event, events.Initialized):
754
886
  handle_initialized(context, event)
887
+ if isinstance(event, events.BeforeProbing):
888
+ handle_before_probing(context, event)
889
+ if isinstance(event, events.AfterProbing):
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)
755
895
  if isinstance(event, events.BeforeExecution):
756
896
  handle_before_execution(context, event)
757
897
  if isinstance(event, events.AfterExecution):
@@ -26,6 +26,14 @@ class ShortOutputStyleHandler(EventHandler):
26
26
  """
27
27
  if isinstance(event, events.Initialized):
28
28
  default.handle_initialized(context, event)
29
+ if isinstance(event, events.BeforeProbing):
30
+ default.handle_before_probing(context, event)
31
+ if isinstance(event, events.AfterProbing):
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)
29
37
  if isinstance(event, events.BeforeExecution):
30
38
  handle_before_execution(context, event)
31
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
@@ -960,9 +960,12 @@ class Response:
960
960
  @classmethod
961
961
  def from_requests(cls, response: requests.Response) -> Response:
962
962
  """Create a response from requests.Response."""
963
- headers = {name: response.raw.headers.getlist(name) for name in response.raw.headers.keys()}
963
+ raw = response.raw
964
+ raw_headers = raw.headers if raw is not None else {}
965
+ headers = {name: response.raw.headers.getlist(name) for name in raw_headers.keys()}
964
966
  # Similar to http.client:319 (HTTP version detection in stdlib's `http` package)
965
- http_version = "1.0" if response.raw.version == 10 else "1.1"
967
+ version = raw.version if raw is not None else 10
968
+ http_version = "1.0" if version == 10 else "1.1"
966
969
 
967
970
  def is_empty(_response: requests.Response) -> bool:
968
971
  # Assume the response is empty if:
@@ -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