schemathesis 3.35.3__py3-none-any.whl → 3.35.5__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 (81) hide show
  1. schemathesis/__init__.py +5 -5
  2. schemathesis/_hypothesis.py +12 -6
  3. schemathesis/_override.py +4 -4
  4. schemathesis/auths.py +1 -1
  5. schemathesis/cli/__init__.py +19 -13
  6. schemathesis/cli/callbacks.py +6 -4
  7. schemathesis/cli/cassettes.py +67 -41
  8. schemathesis/cli/context.py +7 -6
  9. schemathesis/cli/junitxml.py +1 -1
  10. schemathesis/cli/options.py +7 -4
  11. schemathesis/cli/output/default.py +5 -5
  12. schemathesis/cli/reporting.py +4 -2
  13. schemathesis/code_samples.py +4 -3
  14. schemathesis/exceptions.py +4 -3
  15. schemathesis/extra/_flask.py +4 -1
  16. schemathesis/extra/pytest_plugin.py +6 -3
  17. schemathesis/failures.py +2 -1
  18. schemathesis/filters.py +2 -2
  19. schemathesis/generation/__init__.py +2 -2
  20. schemathesis/generation/_hypothesis.py +1 -1
  21. schemathesis/generation/coverage.py +5 -5
  22. schemathesis/graphql.py +0 -1
  23. schemathesis/hooks.py +3 -3
  24. schemathesis/lazy.py +10 -7
  25. schemathesis/loaders.py +3 -3
  26. schemathesis/models.py +39 -15
  27. schemathesis/runner/__init__.py +5 -5
  28. schemathesis/runner/events.py +1 -1
  29. schemathesis/runner/impl/context.py +58 -0
  30. schemathesis/runner/impl/core.py +54 -61
  31. schemathesis/runner/impl/solo.py +17 -20
  32. schemathesis/runner/impl/threadpool.py +65 -71
  33. schemathesis/runner/serialization.py +4 -3
  34. schemathesis/sanitization.py +2 -1
  35. schemathesis/schemas.py +18 -20
  36. schemathesis/serializers.py +2 -0
  37. schemathesis/service/client.py +1 -1
  38. schemathesis/service/events.py +4 -1
  39. schemathesis/service/extensions.py +2 -2
  40. schemathesis/service/hosts.py +4 -2
  41. schemathesis/service/models.py +3 -3
  42. schemathesis/service/report.py +3 -3
  43. schemathesis/service/serialization.py +4 -2
  44. schemathesis/specs/graphql/loaders.py +4 -3
  45. schemathesis/specs/graphql/schemas.py +4 -3
  46. schemathesis/specs/openapi/definitions.py +1 -5
  47. schemathesis/specs/openapi/examples.py +92 -2
  48. schemathesis/specs/openapi/expressions/__init__.py +7 -0
  49. schemathesis/specs/openapi/expressions/extractors.py +4 -1
  50. schemathesis/specs/openapi/expressions/nodes.py +5 -3
  51. schemathesis/specs/openapi/links.py +4 -4
  52. schemathesis/specs/openapi/loaders.py +5 -4
  53. schemathesis/specs/openapi/negative/__init__.py +5 -3
  54. schemathesis/specs/openapi/negative/mutations.py +5 -4
  55. schemathesis/specs/openapi/parameters.py +11 -8
  56. schemathesis/specs/openapi/schemas.py +9 -10
  57. schemathesis/specs/openapi/security.py +6 -4
  58. schemathesis/specs/openapi/stateful/__init__.py +2 -2
  59. schemathesis/specs/openapi/stateful/statistic.py +3 -3
  60. schemathesis/specs/openapi/stateful/types.py +3 -2
  61. schemathesis/stateful/__init__.py +3 -3
  62. schemathesis/stateful/config.py +1 -1
  63. schemathesis/stateful/context.py +3 -3
  64. schemathesis/stateful/events.py +3 -3
  65. schemathesis/stateful/runner.py +5 -4
  66. schemathesis/stateful/sink.py +1 -1
  67. schemathesis/stateful/state_machine.py +5 -5
  68. schemathesis/stateful/statistic.py +3 -1
  69. schemathesis/stateful/validation.py +1 -1
  70. schemathesis/transports/__init__.py +2 -2
  71. schemathesis/transports/asgi.py +7 -0
  72. schemathesis/transports/auth.py +2 -1
  73. schemathesis/transports/content_types.py +1 -1
  74. schemathesis/transports/responses.py +2 -1
  75. schemathesis/utils.py +4 -2
  76. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/METADATA +1 -1
  77. schemathesis-3.35.5.dist-info/RECORD +156 -0
  78. schemathesis-3.35.3.dist-info/RECORD +0 -154
  79. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/WHEEL +0 -0
  80. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/entry_points.txt +0 -0
  81. {schemathesis-3.35.3.dist-info → schemathesis-3.35.5.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py CHANGED
@@ -2,12 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
- from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
5
+ from . import auths, checks, contrib, experimental, fixups, graphql, hooks, runner, serializers, targets
6
6
  from ._lazy_import import lazy_import
7
- from .constants import SCHEMATHESIS_VERSION # noqa: E402
8
- from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig # noqa: E402
9
- from .models import Case # noqa: E402
10
- from .specs import openapi # noqa: E402
7
+ from .constants import SCHEMATHESIS_VERSION
8
+ from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig
9
+ from .models import Case
10
+ from .specs import openapi
11
11
 
12
12
  __version__ = SCHEMATHESIS_VERSION
13
13
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import asyncio
6
6
  import json
7
7
  import warnings
8
- from typing import Any, Callable, Generator, Mapping, Optional, Tuple
8
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
9
9
 
10
10
  import hypothesis
11
11
  from hypothesis import Phase
@@ -24,7 +24,9 @@ from .models import APIOperation, Case, GenerationMetadata, TestPhase
24
24
  from .transports.content_types import parse_content_type
25
25
  from .transports.headers import has_invalid_characters, is_latin_1_encodable
26
26
  from .types import NotSet
27
- from .utils import GivenInput
27
+
28
+ if TYPE_CHECKING:
29
+ from .utils import GivenInput
28
30
 
29
31
  # Forcefully initializes Hypothesis' global PRNG to avoid races that initilize it
30
32
  # if e.g. Schemathesis CLI is used with multiple workers
@@ -215,6 +217,7 @@ def _iter_coverage_cases(
215
217
  operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
216
218
  ) -> Generator[Case, None, None]:
217
219
  from .specs.openapi.constants import LOCATION_TO_CONTAINER
220
+ from .specs.openapi.examples import find_in_responses, find_matching_in_responses
218
221
 
219
222
  ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
220
223
  meta = GenerationMetadata(
@@ -222,8 +225,11 @@ def _iter_coverage_cases(
222
225
  )
223
226
  generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
224
227
  template: dict[str, Any] = {}
228
+ responses = find_in_responses(operation)
225
229
  for parameter in operation.iter_parameters():
226
230
  schema = parameter.as_json_schema(operation)
231
+ for value in find_matching_in_responses(responses, parameter.name):
232
+ schema.setdefault("examples", []).append(value)
227
233
  gen = coverage.cover_schema_iter(ctx, schema)
228
234
  value = next(gen, NOT_SET)
229
235
  if isinstance(value, NotSet):
@@ -274,7 +280,7 @@ def _iter_coverage_cases(
274
280
  yield case
275
281
 
276
282
 
277
- def find_invalid_headers(headers: Mapping) -> Generator[Tuple[str, str], None, None]:
283
+ def find_invalid_headers(headers: Mapping) -> Generator[tuple[str, str], None, None]:
278
284
  for name, value in headers.items():
279
285
  if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
280
286
  yield name, value
@@ -305,11 +311,11 @@ def add_non_serializable_mark(test: Callable, exc: SerializationNotPossible) ->
305
311
  test._schemathesis_non_serializable = exc # type: ignore
306
312
 
307
313
 
308
- def get_non_serializable_mark(test: Callable) -> Optional[SerializationNotPossible]:
314
+ def get_non_serializable_mark(test: Callable) -> SerializationNotPossible | None:
309
315
  return getattr(test, "_schemathesis_non_serializable", None)
310
316
 
311
317
 
312
- def get_invalid_regex_mark(test: Callable) -> Optional[SchemaError]:
318
+ def get_invalid_regex_mark(test: Callable) -> SchemaError | None:
313
319
  return getattr(test, "_schemathesis_invalid_regex", None)
314
320
 
315
321
 
@@ -317,7 +323,7 @@ def add_invalid_regex_mark(test: Callable, exc: SchemaError) -> None:
317
323
  test._schemathesis_invalid_regex = exc # type: ignore
318
324
 
319
325
 
320
- def get_invalid_example_headers_mark(test: Callable) -> Optional[dict[str, str]]:
326
+ def get_invalid_example_headers_mark(test: Callable) -> dict[str, str] | None:
321
327
  return getattr(test, "_schemathesis_invalid_example_headers", None)
322
328
 
323
329
 
schemathesis/_override.py CHANGED
@@ -1,14 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import TYPE_CHECKING, Optional
4
+ from typing import TYPE_CHECKING
5
5
 
6
6
  from .exceptions import UsageError
7
- from .parameters import ParameterSet
8
- from .types import GenericTest
9
7
 
10
8
  if TYPE_CHECKING:
11
9
  from .models import APIOperation
10
+ from .parameters import ParameterSet
11
+ from .types import GenericTest
12
12
 
13
13
 
14
14
  @dataclass
@@ -37,7 +37,7 @@ def _for_parameters(overridden: dict[str, str], defined: ParameterSet) -> dict[s
37
37
  return output
38
38
 
39
39
 
40
- def get_override_from_mark(test: GenericTest) -> Optional[CaseOverride]:
40
+ def get_override_from_mark(test: GenericTest) -> CaseOverride | None:
41
41
  return getattr(test, "_schemathesis_override", None)
42
42
 
43
43
 
schemathesis/auths.py CHANGED
@@ -21,12 +21,12 @@ from typing import (
21
21
 
22
22
  from .exceptions import UsageError
23
23
  from .filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
24
- from .types import GenericTest
25
24
 
26
25
  if TYPE_CHECKING:
27
26
  import requests.auth
28
27
 
29
28
  from .models import APIOperation, Case
29
+ from .types import GenericTest
30
30
 
31
31
  DEFAULT_REFRESH_INTERVAL = 300
32
32
  AUTH_STORAGE_ATTRIBUTE_NAME = "_schemathesis_auth"
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import base64
4
- import io
5
4
  import os
6
5
  import sys
7
6
  import traceback
@@ -41,38 +40,45 @@ from ..internal.datetime import current_datetime
41
40
  from ..internal.output import OutputConfig
42
41
  from ..internal.validation import file_exists
43
42
  from ..loaders import load_app, load_yaml
44
- from ..models import Case, CheckFunction
45
43
  from ..runner import events, prepare_hypothesis_settings, probes
46
44
  from ..specs.graphql import loaders as gql_loaders
47
45
  from ..specs.openapi import loaders as oas_loaders
48
46
  from ..stateful import Stateful
49
- from ..targets import Target
50
47
  from ..transports import RequestConfig
51
48
  from ..transports.auth import get_requests_auth
52
- from ..types import PathLike, RequestCert
53
49
  from . import callbacks, cassettes, output
54
50
  from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
55
51
  from .context import ExecutionContext, FileReportContext, ServiceReportContext
56
52
  from .debug import DebugOutputHandler
57
53
  from .handlers import EventHandler
58
54
  from .junitxml import JunitXMLHandler
59
- from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, NotSet, OptionalInt
55
+ from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, OptionalInt
60
56
  from .sanitization import SanitizationHandler
61
57
 
62
58
  if TYPE_CHECKING:
59
+ import io
60
+
63
61
  import hypothesis
64
62
  import requests
65
63
 
64
+ from ..models import Case, CheckFunction
66
65
  from ..schemas import BaseSchema
67
66
  from ..service.client import ServiceClient
68
67
  from ..specs.graphql.schemas import GraphQLSchema
68
+ from ..targets import Target
69
+ from ..types import NotSet, PathLike, RequestCert
70
+
71
+
72
+ __all__ = [
73
+ "EventHandler",
74
+ ]
69
75
 
70
76
 
71
77
  def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
72
78
  return tuple(item.__name__ for item in items)
73
79
 
74
80
 
75
- CUSTOM_HANDLERS: list[Type[EventHandler]] = []
81
+ CUSTOM_HANDLERS: list[type[EventHandler]] = []
76
82
  CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
77
83
 
78
84
  DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
@@ -114,14 +120,14 @@ def reset_checks() -> None:
114
120
  """Get checks list to their default state."""
115
121
  # Useful in tests
116
122
  checks_module.ALL_CHECKS = checks_module.DEFAULT_CHECKS + checks_module.OPTIONAL_CHECKS
117
- CHECKS_TYPE.choices = _get_callable_names(checks_module.ALL_CHECKS) + ("all",)
123
+ CHECKS_TYPE.choices = (*_get_callable_names(checks_module.ALL_CHECKS), "all")
118
124
 
119
125
 
120
126
  def reset_targets() -> None:
121
127
  """Get targets list to their default state."""
122
128
  # Useful in tests
123
129
  targets_module.ALL_TARGETS = targets_module.DEFAULT_TARGETS + targets_module.OPTIONAL_TARGETS
124
- TARGETS_TYPE.choices = _get_callable_names(targets_module.ALL_TARGETS) + ("all",)
130
+ TARGETS_TYPE.choices = (*_get_callable_names(targets_module.ALL_TARGETS), "all")
125
131
 
126
132
 
127
133
  @click.group(context_settings=CONTEXT_SETTINGS)
@@ -282,7 +288,7 @@ REPORT_TO_SERVICE = ReportToService()
282
288
  "workers_num",
283
289
  help="Number of concurrent workers for testing. Auto-adjusts if 'auto' is specified",
284
290
  type=CustomHelpMessageChoice(
285
- ["auto"] + list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1))),
291
+ ["auto", *list(map(str, range(MIN_WORKERS, MAX_WORKERS + 1)))],
286
292
  choices_repr=f"[auto, {MIN_WORKERS}-{MAX_WORKERS}]",
287
293
  ),
288
294
  default=str(DEFAULT_WORKERS),
@@ -318,7 +324,7 @@ REPORT_TO_SERVICE = ReportToService()
318
324
  "--fixups",
319
325
  help="Apply compatibility adjustments",
320
326
  multiple=True,
321
- type=click.Choice(list(ALL_FIXUPS) + ["all"]),
327
+ type=click.Choice([*ALL_FIXUPS, "all"]),
322
328
  metavar="",
323
329
  )
324
330
  @group("API validation options")
@@ -2000,7 +2006,7 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
2000
2006
  ctx.color = False
2001
2007
 
2002
2008
 
2003
- def add_option(*args: Any, cls: Type = click.Option, **kwargs: Any) -> None:
2009
+ def add_option(*args: Any, cls: type = click.Option, **kwargs: Any) -> None:
2004
2010
  """Add a new CLI option to `st run`."""
2005
2011
  run.params.append(cls(args, **kwargs))
2006
2012
 
@@ -2024,10 +2030,10 @@ def add_group(name: str, *, index: int | None = None) -> Group:
2024
2030
  return Group(name)
2025
2031
 
2026
2032
 
2027
- def handler() -> Callable[[Type], None]:
2033
+ def handler() -> Callable[[type], None]:
2028
2034
  """Register a new CLI event handler."""
2029
2035
 
2030
- def _wrapper(cls: Type) -> None:
2036
+ def _wrapper(cls: type) -> None:
2031
2037
  CUSTOM_HANDLERS.append(cls)
2032
2038
 
2033
2039
  return _wrapper
@@ -2,16 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import codecs
4
4
  import enum
5
+ import operator
5
6
  import os
6
7
  import re
7
8
  import traceback
8
9
  from contextlib import contextmanager
9
- from functools import partial
10
+ from functools import partial, reduce
10
11
  from typing import TYPE_CHECKING, Callable, Generator
11
12
  from urllib.parse import urlparse
12
13
 
13
14
  import click
14
- from click.types import LazyFile # type: ignore
15
15
 
16
16
  from .. import exceptions, experimental, throttling
17
17
  from ..code_samples import CodeSampleStyle
@@ -24,12 +24,14 @@ from ..loaders import load_app
24
24
  from ..service.hosts import get_temporary_hosts_file
25
25
  from ..stateful import Stateful
26
26
  from ..transports.headers import has_invalid_characters, is_latin_1_encodable
27
- from ..types import PathLike
28
27
  from .cassettes import CassetteFormat
29
28
  from .constants import DEFAULT_WORKERS
30
29
 
31
30
  if TYPE_CHECKING:
32
31
  import hypothesis
32
+ from click.types import LazyFile # type: ignore[attr-defined]
33
+
34
+ from ..types import PathLike
33
35
 
34
36
  INVALID_DERANDOMIZE_MESSAGE = (
35
37
  "`--hypothesis-derandomize` implies no database, so passing `--hypothesis-database` too is invalid."
@@ -339,7 +341,7 @@ def convert_experimental(
339
341
 
340
342
 
341
343
  def convert_checks(ctx: click.core.Context, param: click.core.Parameter, value: tuple[list[str]]) -> list[str]:
342
- return sum(value, [])
344
+ return reduce(operator.iadd, value, [])
343
345
 
344
346
 
345
347
  def convert_code_sample_style(ctx: click.core.Context, param: click.core.Parameter, value: str) -> CodeSampleStyle:
@@ -16,7 +16,6 @@ import harfile
16
16
 
17
17
  from ..constants import SCHEMATHESIS_VERSION
18
18
  from ..runner import events
19
- from ..types import RequestCert
20
19
  from .handlers import EventHandler
21
20
 
22
21
  if TYPE_CHECKING:
@@ -25,6 +24,7 @@ if TYPE_CHECKING:
25
24
 
26
25
  from ..models import Request, Response
27
26
  from ..runner.serialization import SerializedCheck, SerializedInteraction
27
+ from ..types import RequestCert
28
28
  from .context import ExecutionContext
29
29
 
30
30
  # Wait until the worker terminates
@@ -163,13 +163,18 @@ def vcr_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
163
163
  return "\n".join(f' "{name}":\n{format_header_values(values)}' for name, values in headers.items())
164
164
 
165
165
  def format_check_message(message: str | None) -> str:
166
- return "~" if message is None else f"{repr(message)}"
166
+ return "~" if message is None else f"{message!r}"
167
167
 
168
168
  def format_checks(checks: list[SerializedCheck]) -> str:
169
- return "\n".join(
169
+ if not checks:
170
+ return " checks: []"
171
+ items = "\n".join(
170
172
  f" - name: '{check.name}'\n status: '{check.value.name.upper()}'\n message: {format_check_message(check.message)}"
171
173
  for check in checks
172
174
  )
175
+ return f"""
176
+ checks:
177
+ {items}"""
173
178
 
174
179
  if preserve_exact_body_bytes:
175
180
 
@@ -235,9 +240,8 @@ http_interactions:"""
235
240
  correlation_id: '{item.correlation_id}'
236
241
  data_generation_method: '{interaction.data_generation_method.value}'
237
242
  phase: {phase}
238
- elapsed: '{interaction.response.elapsed}'
243
+ elapsed: '{interaction.response.elapsed if interaction.response else 0}'
239
244
  recorded_at: '{interaction.recorded_at}'
240
- checks:
241
245
  {format_checks(interaction.checks)}
242
246
  request:
243
247
  uri: '{interaction.request.uri}'
@@ -246,8 +250,9 @@ http_interactions:"""
246
250
  {format_headers(interaction.request.headers)}"""
247
251
  )
248
252
  format_request_body(stream, interaction.request)
249
- stream.write(
250
- f"""
253
+ if interaction.response is not None:
254
+ stream.write(
255
+ f"""
251
256
  response:
252
257
  status:
253
258
  code: '{interaction.response.status_code}'
@@ -255,12 +260,16 @@ http_interactions:"""
255
260
  headers:
256
261
  {format_headers(interaction.response.headers)}
257
262
  """
258
- )
259
- format_response_body(stream, interaction.response)
260
- stream.write(
261
- f"""
263
+ )
264
+ format_response_body(stream, interaction.response)
265
+ stream.write(
266
+ f"""
262
267
  http_version: '{interaction.response.http_version}'"""
263
- )
268
+ )
269
+ else:
270
+ stream.write("""
271
+ response: null
272
+ """)
264
273
  current_id += 1
265
274
  else:
266
275
  break
@@ -300,11 +309,11 @@ def write_double_quoted(stream: IO, text: str) -> None:
300
309
  if ch in Emitter.ESCAPE_REPLACEMENTS:
301
310
  data = "\\" + Emitter.ESCAPE_REPLACEMENTS[ch]
302
311
  elif ch <= "\xff":
303
- data = "\\x%02X" % ord(ch)
312
+ data = f"\\x{ord(ch):02X}"
304
313
  elif ch <= "\uffff":
305
- data = "\\u%04X" % ord(ch)
314
+ data = f"\\u{ord(ch):04X}"
306
315
  else:
307
- data = "\\U%08X" % ord(ch)
316
+ data = f"\\U{ord(ch):08X}"
308
317
  stream.write(data)
309
318
  start = end + 1
310
319
  end += 1
@@ -326,25 +335,45 @@ def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
326
335
  item = queue.get()
327
336
  if isinstance(item, Process):
328
337
  for interaction in item.interactions:
329
- time = round(interaction.response.elapsed * 1000, 2)
330
- content_type = interaction.response.headers.get("Content-Type", [""])[0]
331
- content = harfile.Content(
332
- size=interaction.response.body_size or 0,
333
- mimeType=content_type,
334
- text=get_body(interaction.response.body) if interaction.response.body is not None else None,
335
- encoding="base64"
336
- if interaction.response.body is not None and preserve_exact_body_bytes
337
- else None,
338
- )
339
- http_version = f"HTTP/{interaction.response.http_version}"
340
338
  query_params = urlparse(interaction.request.uri).query
341
339
  if interaction.request.body is not None:
342
340
  post_data = harfile.PostData(
343
- mimeType=content_type,
341
+ mimeType=interaction.request.headers.get("Content-Type", [""])[0],
344
342
  text=get_body(interaction.request.body),
345
343
  )
346
344
  else:
347
345
  post_data = None
346
+ if interaction.response is not None:
347
+ content_type = interaction.response.headers.get("Content-Type", [""])[0]
348
+ content = harfile.Content(
349
+ size=interaction.response.body_size or 0,
350
+ mimeType=content_type,
351
+ text=get_body(interaction.response.body) if interaction.response.body is not None else None,
352
+ encoding="base64"
353
+ if interaction.response.body is not None and preserve_exact_body_bytes
354
+ else None,
355
+ )
356
+ http_version = f"HTTP/{interaction.response.http_version}"
357
+ response = harfile.Response(
358
+ status=interaction.response.status_code,
359
+ httpVersion=http_version,
360
+ statusText=interaction.response.message,
361
+ headers=[
362
+ harfile.Record(name=name, value=values[0])
363
+ for name, values in interaction.response.headers.items()
364
+ ],
365
+ cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
366
+ content=content,
367
+ headersSize=_headers_size(interaction.response.headers),
368
+ bodySize=interaction.response.body_size or 0,
369
+ redirectURL=interaction.response.headers.get("Location", [""])[0],
370
+ )
371
+ time = round(interaction.response.elapsed * 1000, 2)
372
+ else:
373
+ response = HARFILE_NO_RESPONSE
374
+ time = 0
375
+ http_version = ""
376
+
348
377
  har.add_entry(
349
378
  startedDateTime=interaction.recorded_at,
350
379
  time=time,
@@ -365,26 +394,23 @@ def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: boo
365
394
  bodySize=interaction.request.body_size or 0,
366
395
  postData=post_data,
367
396
  ),
368
- response=harfile.Response(
369
- status=interaction.response.status_code,
370
- httpVersion=http_version,
371
- statusText=interaction.response.message,
372
- headers=[
373
- harfile.Record(name=name, value=values[0])
374
- for name, values in interaction.response.headers.items()
375
- ],
376
- cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
377
- content=content,
378
- headersSize=_headers_size(interaction.response.headers),
379
- bodySize=interaction.response.body_size or 0,
380
- redirectURL=interaction.response.headers.get("Location", [""])[0],
381
- ),
397
+ response=response,
382
398
  timings=harfile.Timings(send=0, wait=0, receive=time, blocked=0, dns=0, connect=0, ssl=0),
383
399
  )
384
400
  elif isinstance(item, Finalize):
385
401
  break
386
402
 
387
403
 
404
+ HARFILE_NO_RESPONSE = harfile.Response(
405
+ status=0,
406
+ httpVersion="",
407
+ statusText="",
408
+ headers=[],
409
+ cookies=[],
410
+ content=harfile.Content(),
411
+ )
412
+
413
+
388
414
  def _headers_size(headers: dict[str, list[str]]) -> int:
389
415
  size = 0
390
416
  for name, values in headers.items():
@@ -1,22 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
3
  import shutil
5
4
  from dataclasses import dataclass, field
6
- from queue import Queue
7
5
  from typing import TYPE_CHECKING, Generator
8
6
 
9
7
  from ..code_samples import CodeSampleStyle
10
8
  from ..internal.deprecation import deprecated_property
11
9
  from ..internal.output import OutputConfig
12
- from ..internal.result import Result
13
- from ..runner.probes import ProbeRun
14
- from ..runner.serialization import SerializedTestResult
15
- from ..service.models import AnalysisResult
16
10
 
17
11
  if TYPE_CHECKING:
12
+ import os
13
+ from queue import Queue
14
+
18
15
  import hypothesis
19
16
 
17
+ from ..internal.result import Result
18
+ from ..runner.probes import ProbeRun
19
+ from ..runner.serialization import SerializedTestResult
20
+ from ..service.models import AnalysisResult
20
21
  from ..stateful.sink import StateMachineSink
21
22
 
22
23
 
@@ -11,13 +11,13 @@ from ..exceptions import RuntimeErrorType
11
11
  from ..internal.output import prepare_response_payload
12
12
  from ..models import Status
13
13
  from ..runner import events
14
- from ..runner.serialization import SerializedCheck, SerializedError
15
14
  from .handlers import EventHandler
16
15
  from .reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
17
16
 
18
17
  if TYPE_CHECKING:
19
18
  from click.utils import LazyFile
20
19
 
20
+ from ..runner.serialization import SerializedCheck, SerializedError
21
21
  from .context import ExecutionContext
22
22
 
23
23
 
@@ -1,12 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- from enum import Enum
4
- from typing import Any, NoReturn
3
+ from typing import TYPE_CHECKING, Any, NoReturn
5
4
 
6
5
  import click
7
6
 
8
7
  from ..constants import NOT_SET
9
- from ..types import NotSet
8
+
9
+ if TYPE_CHECKING:
10
+ from enum import Enum
11
+
12
+ from ..types import NotSet
10
13
 
11
14
 
12
15
  class CustomHelpMessageChoice(click.Choice):
@@ -65,4 +68,4 @@ class OptionalInt(click.types.IntRange):
65
68
  int(value)
66
69
  return super().convert(value, param, ctx)
67
70
  except ValueError:
68
- self.fail("%s is not a valid integer or None." % value, param, ctx)
71
+ self.fail(f"{value} is not a valid integer or None.", param, ctx)
@@ -6,7 +6,6 @@ import shutil
6
6
  import textwrap
7
7
  import time
8
8
  from importlib import metadata
9
- from queue import Queue
10
9
  from types import GeneratorType
11
10
  from typing import TYPE_CHECKING, Any, Generator, Literal, cast
12
11
 
@@ -44,6 +43,8 @@ from ..handlers import EventHandler
44
43
  from ..reporting import TEST_CASE_ID_TITLE, get_runtime_error_suggestion, group_by_case, split_traceback
45
44
 
46
45
  if TYPE_CHECKING:
46
+ from queue import Queue
47
+
47
48
  import requests
48
49
 
49
50
  SPINNER_REPETITION_NUMBER = 10
@@ -372,7 +373,7 @@ def display_analysis(context: ExecutionContext) -> None:
372
373
  click.echo()
373
374
  if isinstance(analysis, AnalysisSuccess):
374
375
  click.secho(analysis.message, bold=True)
375
- click.echo("\nAnalysis took: {:.2f}ms".format(analysis.elapsed))
376
+ click.echo(f"\nAnalysis took: {analysis.elapsed:.2f}ms")
376
377
  if analysis.extensions:
377
378
  known = []
378
379
  failed = []
@@ -417,8 +418,8 @@ def display_analysis(context: ExecutionContext) -> None:
417
418
  click.secho("Error\n", fg="red", bold=True)
418
419
  _display_service_network_error(response)
419
420
  click.echo()
420
- return None
421
- elif isinstance(exception, requests.RequestException):
421
+ return
422
+ if isinstance(exception, requests.RequestException):
422
423
  message, extras = extract_requests_exception_details(exception)
423
424
  suggestion = "Please check your network connection and try again."
424
425
  title = "Network Error"
@@ -649,7 +650,6 @@ def wait_for_report_handler(queue: Queue, title: str, timeout: float = service.W
649
650
 
650
651
  def create_spinner(repetitions: int) -> Generator[str, None, None]:
651
652
  """A simple spinner that yields its individual characters."""
652
- assert repetitions > 0, "The number of repetitions should be greater than zero"
653
653
  while True:
654
654
  for ch in "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏":
655
655
  # Skip branch coverage, as it is not possible because of the assertion above
@@ -1,14 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from itertools import groupby
4
- from typing import Callable, Generator, Iterator
4
+ from typing import TYPE_CHECKING, Callable, Generator, Iterator
5
5
 
6
6
  import click
7
7
 
8
- from ..code_samples import CodeSampleStyle
9
8
  from ..exceptions import RuntimeErrorType
10
9
  from ..runner.serialization import SerializedCheck, deduplicate_failures
11
10
 
11
+ if TYPE_CHECKING:
12
+ from ..code_samples import CodeSampleStyle
13
+
12
14
  TEST_CASE_ID_TITLE = "Test Case ID"
13
15
 
14
16
 
@@ -6,11 +6,12 @@ from shlex import quote
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from .constants import SCHEMATHESIS_TEST_CASE_HEADER
9
- from .types import Headers
10
9
 
11
10
  if TYPE_CHECKING:
12
11
  from requests.structures import CaseInsensitiveDict
13
12
 
13
+ from .types import Headers
14
+
14
15
 
15
16
  @lru_cache
16
17
  def get_excluded_headers() -> CaseInsensitiveDict:
@@ -120,9 +121,9 @@ def _generate_requests(
120
121
  url = _escape_single_quotes(url)
121
122
  command = f"requests.{method.lower()}('{url}'"
122
123
  if body:
123
- command += f", data={repr(body)}"
124
+ command += f", data={body!r}"
124
125
  if headers:
125
- command += f", headers={repr(headers)}"
126
+ command += f", headers={headers!r}"
126
127
  if not verify:
127
128
  command += ", verify=False"
128
129
  command += ")"
@@ -5,21 +5,22 @@ import re
5
5
  import traceback
6
6
  from dataclasses import dataclass, field
7
7
  from hashlib import sha1
8
- from json import JSONDecodeError
9
- from types import TracebackType
10
8
  from typing import TYPE_CHECKING, Any, Callable, Generator, NoReturn
11
9
 
12
10
  from .constants import SERIALIZERS_SUGGESTION_MESSAGE
13
- from .failures import FailureContext
14
11
  from .internal.output import truncate_json
15
12
 
16
13
  if TYPE_CHECKING:
14
+ from json import JSONDecodeError
15
+ from types import TracebackType
16
+
17
17
  import hypothesis.errors
18
18
  from graphql.error import GraphQLFormattedError
19
19
  from jsonschema import RefResolutionError, ValidationError
20
20
  from jsonschema import SchemaError as JsonSchemaError
21
21
  from requests import RequestException
22
22
 
23
+ from .failures import FailureContext
23
24
  from .transports.responses import GenericResponse
24
25
 
25
26