schemathesis 3.27.0__py3-none-any.whl → 3.28.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.
schemathesis/__init__.py CHANGED
@@ -3,7 +3,7 @@ from typing import Any
3
3
 
4
4
  from . import auths, checks, experimental, contrib, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
5
5
  from ._lazy_import import lazy_import
6
- from .generation import DataGenerationMethod, GenerationConfig # noqa: E402
6
+ from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig # noqa: E402
7
7
  from .constants import SCHEMATHESIS_VERSION # noqa: E402
8
8
  from .models import Case # noqa: E402
9
9
  from .specs import openapi # noqa: E402
@@ -0,0 +1,4 @@
1
+ if __name__ == "__main__":
2
+ import schemathesis.cli
3
+
4
+ schemathesis.cli.schemathesis()
@@ -55,6 +55,8 @@ class ExecutionContext:
55
55
  report: ServiceReportContext | FileReportContext | None = None
56
56
  probes: list[ProbeRun] | None = None
57
57
  analysis: Result[AnalysisResult, Exception] | None = None
58
+ # Special flag to display a warning about Windows-specific encoding issue
59
+ encountered_windows_encoding_issue: bool = False
58
60
 
59
61
  @deprecated_property(removed_in="4.0", replacement="show_trace")
60
62
  def show_errors_tracebacks(self) -> bool:
@@ -1,19 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
- import base64
4
3
  import platform
5
4
  import textwrap
6
5
  from dataclasses import dataclass, field
7
- from typing import TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, cast
8
7
 
9
8
  from junit_xml import TestCase, TestSuite, to_xml_report_file
10
9
 
10
+ from ..exceptions import RuntimeErrorType, prepare_response_payload
11
11
  from ..models import Status
12
12
  from ..runner import events
13
13
  from ..runner.serialization import SerializedCheck, SerializedError
14
- from ..exceptions import prepare_response_payload, RuntimeErrorType
15
14
  from .handlers import EventHandler
16
- from .reporting import group_by_case, TEST_CASE_ID_TITLE, split_traceback, get_runtime_error_suggestion
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
@@ -65,14 +64,15 @@ def build_failure_message(idx: int, code_sample: str, checks: list[SerializedChe
65
64
  status_code = check.response.status_code
66
65
  reason = get_reason(status_code)
67
66
  message += f"\n[{check.response.status_code}] {reason}:\n"
68
- response_body = check.response.body
69
- if response_body is not None:
70
- if not response_body:
67
+ if check.response.body is not None:
68
+ if not check.response.body:
71
69
  message += "\n <EMPTY>\n"
72
70
  else:
73
71
  encoding = check.response.encoding or "utf8"
74
72
  try:
75
- payload = base64.b64decode(response_body).decode(encoding)
73
+ # Checked that is not None
74
+ body = cast(bytes, check.response.deserialize_body())
75
+ payload = body.decode(encoding)
76
76
  payload = prepare_response_payload(payload)
77
77
  payload = textwrap.indent(f"\n`{payload}`\n", prefix=" ")
78
78
  message += payload
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import base64
4
3
  import os
5
4
  import platform
6
5
  import shutil
@@ -200,6 +199,7 @@ def display_full_traceback_message(error: SerializedError) -> bool:
200
199
  "hypothesis.errors.FailedHealthCheck",
201
200
  "hypothesis.errors.InvalidArgument: Scalar ",
202
201
  "hypothesis.errors.InvalidArgument: min_size=",
202
+ "hypothesis.errors.Unsatisfiable",
203
203
  )
204
204
  )
205
205
  and "can never generate an example, because min_size is larger than Hypothesis supports." not in error.exception
@@ -303,23 +303,38 @@ def display_failures_for_single_test(context: ExecutionContext, result: Serializ
303
303
  reason = get_reason(status_code)
304
304
  response = bold(f"[{check.response.status_code}] {reason}")
305
305
  click.echo(f"\n{response}:")
306
- response_body = check.response.body
307
- if response_body is not None:
308
- if not response_body:
306
+ if check.response.body is not None:
307
+ if not check.response.body:
309
308
  click.echo("\n <EMPTY>")
310
309
  else:
311
310
  encoding = check.response.encoding or "utf8"
312
311
  try:
313
- payload = base64.b64decode(response_body).decode(encoding)
312
+ # Checked that is not None
313
+ body = cast(bytes, check.response.deserialize_body())
314
+ payload = body.decode(encoding)
314
315
  payload = prepare_response_payload(payload)
315
316
  payload = textwrap.indent(f"\n`{payload}`", prefix=" ")
316
317
  click.echo(payload)
317
318
  except UnicodeDecodeError:
318
319
  click.echo("\n <BINARY>")
319
320
 
320
- click.echo(
321
- f"\n{bold('Reproduce with')}: \n\n {code_sample}\n",
322
- )
321
+ try:
322
+ click.echo(
323
+ f"\n{bold('Reproduce with')}: \n\n {code_sample}\n",
324
+ )
325
+ except UnicodeEncodeError:
326
+ # On Windows it may fail when redirecting the output to a file
327
+ # because it uses a different encoding than the console encoding and the default
328
+ # is cp1252, which doesn't support some Unicode characters.
329
+ # In this case, display a stub message and set a flag to display a warning at the end
330
+ if platform.system() != "Windows":
331
+ raise
332
+ click.echo(
333
+ f"\n{bold('Reproduce with')}: \n\n"
334
+ " CAN NOT DISPLAY THIS CODE SAMPLE DUE TO TERMINAL LIMITATIONS.\n"
335
+ " SEE DETAILS AT THE END OF THE OUTPUT\n",
336
+ )
337
+ context.encountered_windows_encoding_issue = True
323
338
 
324
339
 
325
340
  def display_application_logs(context: ExecutionContext, event: events.Finished) -> None:
@@ -465,6 +480,17 @@ def display_statistic(context: ExecutionContext, event: events.Finished) -> None
465
480
  seed_option = f"`--hypothesis-seed={context.seed}`"
466
481
  click.secho(f"\n{bold('Note')}: To replicate these test failures, rerun with {bold(seed_option)}")
467
482
 
483
+ if context.encountered_windows_encoding_issue:
484
+ click.echo()
485
+ title = click.style("WARNING:", bold=True, fg="yellow")
486
+ name = click.style("PYTHONIOENCODING", bold=True)
487
+ value = click.style("utf8", bold=True)
488
+ warning = (
489
+ "Some code samples could not be displayed due to terminal limitations on Windows.\n"
490
+ f"To resolve this set the '{name}' environment variable to '{value}' and rerun your command."
491
+ )
492
+ click.secho(f"{title} {warning}")
493
+
468
494
  if context.report is not None and not context.is_interrupted:
469
495
  if isinstance(context.report, FileReportContext):
470
496
  click.echo()
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import base64
4
3
  from itertools import groupby
5
4
  from typing import Callable, Generator
6
5
 
@@ -22,11 +21,10 @@ def group_by_case(
22
21
 
23
22
 
24
23
  def _by_unique_code_sample(check: SerializedCheck, code_sample_style: CodeSampleStyle) -> str:
25
- request_body = base64.b64decode(check.example.body).decode() if check.example.body is not None else None
26
24
  return code_sample_style.generate(
27
25
  method=check.example.method,
28
26
  url=check.example.url,
29
- body=request_body,
27
+ body=check.example.deserialize_body(),
30
28
  headers=check.example.headers,
31
29
  verify=check.example.verify,
32
30
  extra_headers=check.example.extra_headers,
@@ -336,6 +336,12 @@ class DeadlineExceeded(Exception):
336
336
  )
337
337
 
338
338
 
339
+ class RecursiveReferenceError(Exception):
340
+ """Recursive reference is impossible to resolve due to current limitations."""
341
+
342
+ __module__ = "builtins"
343
+
344
+
339
345
  @enum.unique
340
346
  class RuntimeErrorType(str, enum.Enum):
341
347
  # Connection related issues
@@ -354,6 +360,7 @@ class RuntimeErrorType(str, enum.Enum):
354
360
 
355
361
  SCHEMA_BODY_IN_GET_REQUEST = "schema_body_in_get_request"
356
362
  SCHEMA_INVALID_REGULAR_EXPRESSION = "schema_invalid_regular_expression"
363
+ SCHEMA_UNSUPPORTED = "schema_unsupported"
357
364
  SCHEMA_GENERIC = "schema_generic"
358
365
 
359
366
  SERIALIZATION_NOT_POSSIBLE = "serialization_not_possible"
@@ -367,6 +374,7 @@ class RuntimeErrorType(str, enum.Enum):
367
374
  return self not in (
368
375
  RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST,
369
376
  RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION,
377
+ RuntimeErrorType.SCHEMA_UNSUPPORTED,
370
378
  RuntimeErrorType.SCHEMA_GENERIC,
371
379
  RuntimeErrorType.SERIALIZATION_NOT_POSSIBLE,
372
380
  )
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
  import random
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, field
4
4
  from enum import Enum
5
- from typing import Union, Iterable
5
+ from typing import Union, Iterable, TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from hypothesis.strategies import SearchStrategy
6
9
 
7
10
 
8
11
  class DataGenerationMethod(str, Enum):
@@ -58,6 +61,13 @@ def generate_random_case_id(length: int = 6) -> str:
58
61
  return output
59
62
 
60
63
 
64
+ @dataclass
65
+ class HeaderConfig:
66
+ """Configuration for generating headers."""
67
+
68
+ strategy: SearchStrategy[str] | None = None
69
+
70
+
61
71
  @dataclass
62
72
  class GenerationConfig:
63
73
  """Holds various configuration options relevant for data generation."""
@@ -66,3 +76,5 @@ class GenerationConfig:
66
76
  allow_x00: bool = True
67
77
  # Generate strings using the given codec
68
78
  codec: str | None = "utf-8"
79
+ # Header generation configuration
80
+ headers: HeaderConfig = field(default_factory=HeaderConfig)
schemathesis/models.py CHANGED
@@ -50,11 +50,11 @@ from .exceptions import (
50
50
  from .generation import DataGenerationMethod, GenerationConfig, generate_random_case_id
51
51
  from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher, dispatch
52
52
  from .internal.copy import fast_deepcopy
53
- from .internal.deprecation import deprecated_property, deprecated_function
53
+ from .internal.deprecation import deprecated_function, deprecated_property
54
54
  from .parameters import Parameter, ParameterSet, PayloadAlternatives
55
55
  from .sanitization import sanitize_request, sanitize_response
56
56
  from .serializers import Serializer
57
- from .transports import ASGITransport, RequestsTransport, WSGITransport, serialize_payload
57
+ from .transports import ASGITransport, RequestsTransport, WSGITransport, serialize_payload, deserialize_payload
58
58
  from .types import Body, Cookies, FormData, Headers, NotSet, PathParameters, Query
59
59
 
60
60
  if TYPE_CHECKING:
@@ -854,6 +854,14 @@ class Request:
854
854
  body=serialize_payload(body) if body is not None else body,
855
855
  )
856
856
 
857
+ def deserialize_body(self) -> bytes | None:
858
+ """Deserialize the request body.
859
+
860
+ `Request` should be serializable to JSON, therefore body is encoded as base64 string
861
+ to support arbitrary binary data.
862
+ """
863
+ return deserialize_payload(self.body)
864
+
857
865
 
858
866
  @dataclass(repr=False)
859
867
  class Response:
@@ -923,6 +931,14 @@ class Response:
923
931
  verify=True,
924
932
  )
925
933
 
934
+ def deserialize_body(self) -> bytes | None:
935
+ """Deserialize the response body.
936
+
937
+ `Response` should be serializable to JSON, therefore body is encoded as base64 string
938
+ to support arbitrary binary data.
939
+ """
940
+ return deserialize_payload(self.body)
941
+
926
942
 
927
943
  @dataclass
928
944
  class Interaction:
@@ -42,10 +42,12 @@ from ...constants import (
42
42
  from ...exceptions import (
43
43
  CheckFailed,
44
44
  DeadlineExceeded,
45
+ InternalError,
45
46
  InvalidHeadersExample,
46
47
  InvalidRegularExpression,
47
48
  NonCheckError,
48
49
  OperationSchemaError,
50
+ RecursiveReferenceError,
49
51
  SerializationNotPossible,
50
52
  SkipTest,
51
53
  format_exception,
@@ -299,10 +301,7 @@ def run_probes(schema: BaseSchema, config: probes.ProbeConfig) -> list[probes.Pr
299
301
  if isinstance(result.probe, probes.NullByteInHeader) and result.is_failure:
300
302
  from ...specs.openapi._hypothesis import HEADER_FORMAT, header_values
301
303
 
302
- formats.register(
303
- HEADER_FORMAT,
304
- header_values(blacklist_characters="\n\r\x00").map(str.lstrip),
305
- )
304
+ formats.register(HEADER_FORMAT, header_values(blacklist_characters="\n\r\x00"))
306
305
  return results
307
306
 
308
307
 
@@ -477,13 +476,25 @@ def run_test(
477
476
  except SkipTest as exc:
478
477
  status = Status.skip
479
478
  result.mark_skipped(exc)
480
- except AssertionError: # comes from `hypothesis-jsonschema`
481
- error = reraise(operation)
479
+ except AssertionError as exc: # May come from `hypothesis-jsonschema` or `hypothesis`
482
480
  status = Status.error
481
+ try:
482
+ operation.schema.validate()
483
+ try:
484
+ raise InternalError(f"Unexpected error during testing of this API operation: {exc}") from exc
485
+ except InternalError as exc:
486
+ error = exc
487
+ except ValidationError as exc:
488
+ error = OperationSchemaError.from_jsonschema_error(
489
+ exc,
490
+ path=operation.path,
491
+ method=operation.method,
492
+ full_path=operation.schema.get_full_path(operation.path),
493
+ )
483
494
  result.add_error(error)
484
495
  except HypothesisRefResolutionError:
485
496
  status = Status.error
486
- result.add_error(hypothesis.errors.Unsatisfiable(RECURSIVE_REFERENCE_ERROR_MESSAGE))
497
+ result.add_error(RecursiveReferenceError(RECURSIVE_REFERENCE_ERROR_MESSAGE))
487
498
  except InvalidArgument as error:
488
499
  status = Status.error
489
500
  message = get_invalid_regular_expression_message(warnings)
@@ -617,16 +628,6 @@ def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> st
617
628
  return None
618
629
 
619
630
 
620
- def reraise(operation: APIOperation) -> OperationSchemaError:
621
- try:
622
- operation.schema.validate()
623
- except ValidationError as exc:
624
- return OperationSchemaError.from_jsonschema_error(
625
- exc, path=operation.path, method=operation.method, full_path=operation.schema.get_full_path(operation.path)
626
- )
627
- return OperationSchemaError("Unknown schema error")
628
-
629
-
630
631
  MEMORY_ADDRESS_RE = re.compile("0x[0-9a-fA-F]+")
631
632
  URL_IN_ERROR_MESSAGE_RE = re.compile(r"Max retries exceeded with url: .*? \(Caused by")
632
633
 
@@ -19,6 +19,7 @@ from ..exceptions import (
19
19
  InternalError,
20
20
  InvalidRegularExpression,
21
21
  OperationSchemaError,
22
+ RecursiveReferenceError,
22
23
  RuntimeErrorType,
23
24
  SerializationError,
24
25
  UnboundPrefixError,
@@ -27,7 +28,7 @@ from ..exceptions import (
27
28
  make_unique_by_key,
28
29
  )
29
30
  from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
30
- from ..transports import serialize_payload
31
+ from ..transports import serialize_payload, deserialize_payload
31
32
 
32
33
  if TYPE_CHECKING:
33
34
  import hypothesis.errors
@@ -81,6 +82,14 @@ class SerializedCase:
81
82
  extra_headers=request_data.headers,
82
83
  )
83
84
 
85
+ def deserialize_body(self) -> bytes | None:
86
+ """Deserialize the test case body.
87
+
88
+ `SerializedCase` should be serializable to JSON, therefore body is encoded as base64 string
89
+ to support arbitrary binary data.
90
+ """
91
+ return deserialize_payload(self.body)
92
+
84
93
 
85
94
  def _serialize_body(body: str | bytes | None) -> str | None:
86
95
  if body is None:
@@ -244,6 +253,11 @@ class SerializedError:
244
253
  type_ = RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED
245
254
  message = str(exception).strip()
246
255
  extras = []
256
+ elif isinstance(exception, RecursiveReferenceError):
257
+ type_ = RuntimeErrorType.SCHEMA_UNSUPPORTED
258
+ message = str(exception).strip()
259
+ extras = []
260
+ title = "Unsupported Schema"
247
261
  elif isinstance(exception, hypothesis.errors.InvalidArgument) and str(exception).startswith("Scalar "):
248
262
  # Comes from `hypothesis-graphql`
249
263
  scalar_name = _scalar_name_from_error(exception)
@@ -264,6 +278,7 @@ class SerializedError:
264
278
  message = f"{exception}. Possible reasons:"
265
279
  extras = [
266
280
  "- Contradictory schema constraints, such as a minimum value exceeding the maximum.",
281
+ "- Invalid schema definitions for headers or cookies, for example allowing for non-ASCII characters.",
267
282
  "- Excessive schema complexity, which hinders parameter generation.",
268
283
  ]
269
284
  title = "Schema Error"
@@ -241,7 +241,7 @@ def _encode_multipart(value: Any, boundary: str) -> bytes:
241
241
  return body.getvalue()
242
242
 
243
243
 
244
- @register("multipart/form-data")
244
+ @register("multipart/form-data", aliases=("multipart/mixed",))
245
245
  class MultipartSerializer:
246
246
  def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
247
247
  if isinstance(value, bytes):
@@ -45,7 +45,8 @@ StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str], GenerationC
45
45
  def header_values(blacklist_characters: str = "\n\r") -> st.SearchStrategy[str]:
46
46
  return st.text(
47
47
  alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters=blacklist_characters)
48
- )
48
+ # Header values with leading non-visible chars can't be sent with `requests`
49
+ ).map(str.lstrip)
49
50
 
50
51
 
51
52
  @lru_cache
@@ -67,8 +68,7 @@ def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
67
68
  "_header_name": st.text(
68
69
  min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)
69
70
  ),
70
- # Header values with leading non-visible chars can't be sent with `requests`
71
- HEADER_FORMAT: header_value.map(str.lstrip),
71
+ HEADER_FORMAT: header_value,
72
72
  "_basic_auth": st.tuples(latin1_text, latin1_text).map(make_basic_auth_str),
73
73
  "_bearer_auth": header_value.map("Bearer {}".format),
74
74
  }
@@ -425,6 +425,15 @@ def jsonify_python_specific_types(value: dict[str, Any]) -> dict[str, Any]:
425
425
  return value
426
426
 
427
427
 
428
+ def _build_custom_formats(
429
+ custom_formats: dict[str, st.SearchStrategy] | None, generation_config: GenerationConfig
430
+ ) -> dict[str, st.SearchStrategy]:
431
+ custom_formats = {**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})}
432
+ if generation_config.headers.strategy is not None:
433
+ custom_formats[HEADER_FORMAT] = generation_config.headers.strategy
434
+ return custom_formats
435
+
436
+
428
437
  def make_positive_strategy(
429
438
  schema: dict[str, Any],
430
439
  operation_name: str,
@@ -441,9 +450,10 @@ def make_positive_strategy(
441
450
  for sub_schema in schema.get("properties", {}).values():
442
451
  if list(sub_schema) == ["type"] and sub_schema["type"] == "string":
443
452
  sub_schema.setdefault("format", HEADER_FORMAT)
453
+ custom_formats = _build_custom_formats(custom_formats, generation_config)
444
454
  return from_schema(
445
455
  schema,
446
- custom_formats={**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})},
456
+ custom_formats=custom_formats,
447
457
  allow_x00=generation_config.allow_x00,
448
458
  codec=generation_config.codec,
449
459
  )
@@ -462,12 +472,13 @@ def make_negative_strategy(
462
472
  generation_config: GenerationConfig,
463
473
  custom_formats: dict[str, st.SearchStrategy] | None = None,
464
474
  ) -> st.SearchStrategy:
475
+ custom_formats = _build_custom_formats(custom_formats, generation_config)
465
476
  return negative_schema(
466
477
  schema,
467
478
  operation_name=operation_name,
468
479
  location=location,
469
480
  media_type=media_type,
470
- custom_formats={**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})},
481
+ custom_formats=custom_formats,
471
482
  generation_config=generation_config,
472
483
  )
473
484
 
@@ -186,7 +186,7 @@ def remove_optional_references(schema: dict[str, Any]) -> None:
186
186
  v = s.get(keyword)
187
187
  if v is not None:
188
188
  elided = [sub for sub in v if not can_elide(sub)]
189
- if len(elided) == 1 and "$ref" in elided[0]:
189
+ if len(elided) == 1 and contains_ref(elided[0]):
190
190
  found.append(keyword)
191
191
  return found
192
192
 
@@ -1065,7 +1065,7 @@ class OpenApi30(SwaggerV20):
1065
1065
  # the "multipart/form-data" media type, or any other more general media type that matches it (like `*/*`)
1066
1066
  for media_type, entry in content.items():
1067
1067
  main, sub = parse_content_type(media_type)
1068
- if main in ("*", "multipart") and sub in ("*", "form-data"):
1068
+ if main in ("*", "multipart") and sub in ("*", "form-data", "mixed"):
1069
1069
  schema = entry["schema"]
1070
1070
  break
1071
1071
  else:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import base64
4
4
  import time
5
+ from inspect import iscoroutinefunction
5
6
  from contextlib import contextmanager
6
7
  from dataclasses import dataclass
7
8
  from datetime import timedelta
@@ -29,13 +30,19 @@ def serialize_payload(payload: bytes) -> str:
29
30
  return base64.b64encode(payload).decode()
30
31
 
31
32
 
33
+ def deserialize_payload(data: str | None) -> bytes | None:
34
+ if data is None:
35
+ return None
36
+ return base64.b64decode(data)
37
+
38
+
32
39
  def get(app: Any) -> Transport:
33
40
  """Get transport to send the data to the application."""
34
- from starlette.applications import Starlette
35
-
36
41
  if app is None:
37
42
  return RequestsTransport()
38
- if isinstance(app, Starlette):
43
+ if iscoroutinefunction(app) or (
44
+ hasattr(app, "__call__") and iscoroutinefunction(app.__call__) # noqa: B004
45
+ ):
39
46
  return ASGITransport(app=app)
40
47
  return WSGITransport(app=app)
41
48
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: schemathesis
3
- Version: 3.27.0
3
+ Version: 3.28.0
4
4
  Summary: Property-based testing framework for Open API and GraphQL based apps
5
5
  Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
6
6
  Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
@@ -28,7 +28,6 @@ Classifier: Programming Language :: Python :: 3.12
28
28
  Classifier: Programming Language :: Python :: Implementation :: CPython
29
29
  Classifier: Topic :: Software Development :: Testing
30
30
  Requires-Python: >=3.8
31
- Requires-Dist: anyio<4
32
31
  Requires-Dist: backoff<3.0,>=2.1.2
33
32
  Requires-Dist: click<9.0,>=7.0
34
33
  Requires-Dist: colorama<1.0,>=0.4
@@ -44,7 +43,7 @@ Requires-Dist: pytest-subtests<0.8.0,>=0.2.1
44
43
  Requires-Dist: pytest<9,>=4.6.4
45
44
  Requires-Dist: pyyaml<7.0,>=5.1
46
45
  Requires-Dist: requests<3,>=2.22
47
- Requires-Dist: starlette-testclient<1
46
+ Requires-Dist: starlette-testclient<1,>=0.4.1
48
47
  Requires-Dist: starlette<1,>=0.13
49
48
  Requires-Dist: tomli-w<2.0,>=1.0.0
50
49
  Requires-Dist: tomli<3.0,>=2.0.1
@@ -78,7 +77,7 @@ Requires-Dist: trustme<1.0,>=0.9.0; extra == 'tests'
78
77
  Description-Content-Type: text/markdown
79
78
 
80
79
  <p align="center">
81
- <em>Schemathesis: catch crashes, validate specs, and save time</em>
80
+ <em>Schemathesis: Supercharge your API testing, catch bugs, and ensure compliance</em>
82
81
  </p>
83
82
 
84
83
  <p align="center">
@@ -110,29 +109,34 @@ Description-Content-Type: text/markdown
110
109
 
111
110
  ---
112
111
 
113
- ## Why Schemathesis?
112
+ ## What is Schemathesis?
114
113
 
115
- Schemathesis is a tool that automates your API testing to catch crashes and spec violations. Built on top of the widely-used <a href="https://hypothesis.works/" target="_blank">Hypothesis</a> framework for property-based testing, it offers the following advantages:
114
+ Schemathesis is a tool that levels-up your API testing by automating the process of finding crashes, uncovering bugs, and validating spec compliance. With Schemathesis, you can:
116
115
 
117
- 🕒 **Time-Saving**
116
+ 🎯 **Catch Hard-to-Find Bugs**
118
117
 
119
- Automatically generates test cases, freeing you from manual test writing.
118
+ - Uncover hidden crashes and edge cases that manual testing might miss
119
+ - Identify spec violations and ensure your API adheres to its defined contract
120
120
 
121
- 🔍 **Comprehensive**
121
+ **Accelerate Testing Cycles**
122
122
 
123
- Utilizes fuzzing techniques to probe both common and edge-case scenarios, including those you might overlook.
123
+ - Automatically generate a wide range of test cases based on your API schema
124
+ - Save time and effort by eliminating the need for manual test case creation
124
125
 
125
- 🛠️ **Flexible**
126
+ 🧩 **Integrate Seamlessly**
126
127
 
127
- Supports OpenAPI, GraphQL, and can work even with partially complete schemas. Only the parts describing data generation or responses are required.
128
+ - Works with popular API formats such as OpenAPI, GraphQL.
129
+ - Easily integrate into your existing testing pipeline and CI/CD workflows
128
130
 
129
- 🎛️ **Customizable**
131
+ 🔧 **Customize and Extend**
130
132
 
131
- Customize the framework by writing Python extensions to modify almost any aspect of the testing process.
133
+ - Tune the testing process to your specific requirements using Python extensions
134
+ - Modify and enhance various aspects of the testing flow to suit your needs with rich configuration options
132
135
 
133
- 🔄 **Reproducible**
136
+ 📊 **Gain Valuable Insights**
134
137
 
135
- Generates code samples to help you quickly replicate and investigate any failing test cases.
138
+ - Get detailed reports and actionable insights to help you identify and fix issues quickly
139
+ - Reproduce failing test cases effortlessly with generated code samples and cURL commands
136
140
 
137
141
  ## Quick Demo
138
142
 
@@ -310,6 +314,24 @@ This includes:
310
314
 
311
315
  To discuss a custom support arrangement that best suits your organization, please contact our support team at <a href="mailto:support@schemathesis.io">support@schemathesis.io</a>.
312
316
 
317
+ ## Acknowledgements
318
+
319
+ Schemathesis is built on top of <a href="https://hypothesis.works/" target="_blank">Hypothesis</a>, a powerful property-based testing library for Python.
320
+
321
+ ## Who's Using Schemathesis?
322
+
323
+ Schemathesis is used by a number of project and companies, including direct usage or integration into other tools:
324
+
325
+ - Abstract Machines ([Magistrala](https://github.com/absmach/magistrala))
326
+ - Bundesstelle für Open Data ([smard-api](https://github.com/bundesAPI/smard-api))
327
+ - [CheckMK](https://github.com/Checkmk/checkmk)
328
+ - HXSecurity ([DongTai](https://github.com/HXSecurity/DongTai))
329
+ - Netflix ([Dispatch](https://github.com/Netflix/dispatch))
330
+ - [Pixie](https://github.com/pixie-io/pixie)
331
+ - [Qdrant](https://github.com/qdrant/qdrant)
332
+ - Spotify ([Backstage](https://github.com/backstage/backstage))
333
+ - WordPress ([OpenVerse](https://github.com/WordPress/openverse))
334
+
313
335
  ## Additional content
314
336
 
315
337
  ### Papers
@@ -1,4 +1,4 @@
1
- schemathesis/__init__.py,sha256=WW1NBZxmc5jRR0c24xz-ZtTkJMUQG5MOw1MGVRRGc1s,1970
1
+ schemathesis/__init__.py,sha256=zAa981OnVdIdjiETPP5a5vZL8QXAh2HbTWj8iEoxKC4,1984
2
2
  schemathesis/_compat.py,sha256=VizWR0QAVj4l7WZautNe_zmo3AJ7WBl2zrJQOfJAQtk,1837
3
3
  schemathesis/_dependency_versions.py,sha256=InIv6MZmuRVHc_9FxAiRg7_dY-vuF0jT69FBxrBLK6U,879
4
4
  schemathesis/_hypothesis.py,sha256=O3rfMbT0rChFONMUsYmMEGV9nPG5cHd_6v9NhWDI_IQ,10763
@@ -10,36 +10,37 @@ schemathesis/auths.py,sha256=tUuaHvXO96HJr22Gu9OmlSpL2wbAqKZr3TRbb9dVyA4,14732
10
10
  schemathesis/checks.py,sha256=SYts1Teecg-5kSHBo32Pzhh7YQ4a1Y7DIfldd-0VTj8,2155
11
11
  schemathesis/code_samples.py,sha256=QFwyA7dmc_ij2FRwfbe0uMRm6j2yeofQ8zXynZV3SZI,4122
12
12
  schemathesis/constants.py,sha256=gptQZnXJwEJjuLgw62n7LL_kbkE7dhxUgyyJw_dEEAc,2672
13
- schemathesis/exceptions.py,sha256=fAkismvT-QcIunr6rZ6OllhXFK3sHjwiZJlD63fttQw,19631
13
+ schemathesis/exceptions.py,sha256=-u7pnntcv2OBuY0aO4SGRRtGk2Bd8RqdZuUTSnBzZjk,19882
14
14
  schemathesis/failures.py,sha256=oKbIDD075ymLT4h7SQK-herHDT608d-StPX9pXmOSVU,5996
15
15
  schemathesis/filters.py,sha256=9SJyZ8mcuDyKG2G1hp2Nfz-TvyJWP_NcntsgwFWYY_Q,10250
16
16
  schemathesis/graphql.py,sha256=YkoKWY5K8lxp7H3ikAs-IsoDbiPwJvChG7O8p3DgwtI,229
17
17
  schemathesis/hooks.py,sha256=cNJgCh7SyLWT1sYDKF5ncDC80ld08CinvKo2IqLMV4g,12396
18
18
  schemathesis/lazy.py,sha256=CivWpvesh4iYLSkatXbQPTEOruWmXvuZQ29gng5p9wM,14846
19
19
  schemathesis/loaders.py,sha256=RJnrbf-3vZ7KXmRBkmr3uqWyg0eHzOnONABuudWcTIg,4586
20
- schemathesis/models.py,sha256=KtWx7k9M8wrkaCZ82kfLMKd7jmvFxZnGyUuTE_nPhU0,42974
20
+ schemathesis/models.py,sha256=K5Ct8NhpN0UmjNRVLnb5W_d5tMCu0IYZxR3Q8oE6L4Y,43565
21
21
  schemathesis/parameters.py,sha256=VheEffVzoSfYaSEcG7KhPlA4ypifosG8biiHifzwL8g,2257
22
22
  schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  schemathesis/sanitization.py,sha256=WSV_MB5YRrYkp1pQRPHrzsidqsKcqYZiq64N9grKobo,8956
24
24
  schemathesis/schemas.py,sha256=RzxAoBoJJSLsixrkTZPe2N_oWbWv8n5RL4XaYRyoSIo,18531
25
- schemathesis/serializers.py,sha256=_xoebWkVrgbGbPjPgTgwuN-fN4YT004aj7kImyPComY,11619
25
+ schemathesis/serializers.py,sha256=kxXZ-UGa1v_vOm0sC4QYcrNv4rfvI7tHGT2elRVbCbc,11649
26
26
  schemathesis/targets.py,sha256=tzp7VZ2-7g2nZHCooRgFzTMtOVcbl0rvtNR421hQthA,1162
27
27
  schemathesis/throttling.py,sha256=uwhL4XWPWAU8HECg0NhibfCMn5dT7NElTx3fdL3Mmcc,1065
28
28
  schemathesis/types.py,sha256=AglR5M0bce-YXeDRkweToXTP0GjNOWVjS_mIsxLobwc,919
29
29
  schemathesis/utils.py,sha256=4HXvHysnHp-Uz2HfNgLfW5F5VjL-mtixrjjzRCEJhYo,5233
30
30
  schemathesis/cli/__init__.py,sha256=oTBqweV5CP681LqRFdRXtkd0VyYoG_xUKshkKEPnqIQ,64871
31
+ schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
31
32
  schemathesis/cli/callbacks.py,sha256=HmD0WmSYf5x8v4xeZdOKzy_8CEk23NitUlL8JQ7LHdQ,14950
32
33
  schemathesis/cli/cassettes.py,sha256=1ucgYyOZiNu44uOv044FdfKTzQolUyhAM2onk4m3MD0,12988
33
34
  schemathesis/cli/constants.py,sha256=XoezT0_fHuiOY2e--cmBUhNieJsepcUoW8e48QuLSDI,1544
34
- schemathesis/cli/context.py,sha256=wwfdSL_NKHEyzQnj8PcnaJdzasa_N08e2KPvy4Wl8N4,1874
35
+ schemathesis/cli/context.py,sha256=Hsbt7YS1Gr01QJ825mECpEduEbRIaRRMUz7k6j1SpVI,2005
35
36
  schemathesis/cli/debug.py,sha256=PDEa-oHyz5bZ8aYjRYARwQaCv_AC6HM_L43LJfe4vUM,657
36
37
  schemathesis/cli/handlers.py,sha256=62GPWPmgeGUz3pkDd01H4UCXcIy5a9yRtU7qaAeeR-A,389
37
- schemathesis/cli/junitxml.py,sha256=JMMtWz7grPQw66Ruh-tLR8YVn-58Q0o0l_MxZWQ4V3M,4586
38
+ schemathesis/cli/junitxml.py,sha256=WdxtBKBi6V5Ow840Nde21MvUlDJQyJu2M6DXx1VDuyo,4648
38
39
  schemathesis/cli/options.py,sha256=7_dXcrPT0kWqAkm60cAT1J0IsTOcvNFxT1pcHYttBuI,2558
39
- schemathesis/cli/reporting.py,sha256=uTtoEJT4maJRGPsVZH184S0hohX-4SwO_dAL__i6H10,3401
40
+ schemathesis/cli/reporting.py,sha256=_Jh8sJMfOf_CUHdbTpG5VRsfW8INtXQjtk1njmGWEnY,3298
40
41
  schemathesis/cli/sanitization.py,sha256=v-9rsMCBpWhom0Bfzj_8c6InqxkVjSWK6kRoURa52Nk,727
41
42
  schemathesis/cli/output/__init__.py,sha256=AXaUzQ1nhQ-vXhW4-X-91vE2VQtEcCOrGtQXXNN55iQ,29
42
- schemathesis/cli/output/default.py,sha256=GjT-j9pNxVYQmU6nOu08hBqvR9uD_TMI15jZitWgI0A,36781
43
+ schemathesis/cli/output/default.py,sha256=130qttjuzUgYcCD-LnTaMTumoOMWLzu3q-aPOyJ0ogs,38176
43
44
  schemathesis/cli/output/short.py,sha256=MmFMOKBRqqvgq7Kmx6XJc7Vqkr9g5BrJBxRbyZ0Rh-k,2068
44
45
  schemathesis/contrib/__init__.py,sha256=FH8NL8NXgSKBFOF8Jy_EB6T4CJEaiM-tmDhz16B2o4k,187
45
46
  schemathesis/contrib/unique_data.py,sha256=zxrmAlQH7Bcil9YSfy2EazwLj2rxLzOvAE3O6QRRkFY,1317
@@ -56,7 +57,7 @@ schemathesis/extra/pytest_plugin.py,sha256=44RRkjkN1JupWu0muq9B5b3K9goucYndB394Q
56
57
  schemathesis/fixups/__init__.py,sha256=iTpOm-cwyZQRglbWRJHxj3k7WO9a4a-MCq-UDQa6U0Y,966
57
58
  schemathesis/fixups/fast_api.py,sha256=-gjYNSUtiKl7cKgyQ_rON0Oo_sGm6r7rOQDH0ahG32A,1322
58
59
  schemathesis/fixups/utf8_bom.py,sha256=ttzPOaJb6K7QtNAbIZ_TWrrm0N9FylS8QzO6zI0jXgY,764
59
- schemathesis/generation/__init__.py,sha256=NOmYWmbTt0uW5Vlx2-xfoXV_lLQs08d9T12v9pFBhrA,1951
60
+ schemathesis/generation/__init__.py,sha256=UXRrDTaUEX29VEK9bLqVi_xVzyNNqul3WhpLh1za90M,2277
60
61
  schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
61
62
  schemathesis/internal/copy.py,sha256=xsC7RyrFa9KadMsj9ai_aAy0XZg0oplz3cFZCka_2fg,459
62
63
  schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
@@ -68,9 +69,9 @@ schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QA
68
69
  schemathesis/runner/__init__.py,sha256=kkqjC_5G2Mrq00syLNUDHP3sXqwiId8_cusKtIlOyXM,21419
69
70
  schemathesis/runner/events.py,sha256=eZc4TT8C8mWCMr9h4JvlijvQQ-x8wFry8LntdsYIL-A,10200
70
71
  schemathesis/runner/probes.py,sha256=t_B38-ljy-p3Odw-dqcZUVIjYTPwBvac8KE5GaLnz4M,5546
71
- schemathesis/runner/serialization.py,sha256=7DBve7E6JAbAHH5npzjME7lFnysKCc9dPXMmdIIYrNI,16667
72
+ schemathesis/runner/serialization.py,sha256=NkeXNQUvzpnTiT4TbojpUC0QXl2Gjv7quPx5HUxpJXI,17356
72
73
  schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
73
- schemathesis/runner/impl/core.py,sha256=w0Ky7DuOQD3qpMPBP2E5Pr04d5N_eKyKNyCVZAEEZJc,39684
74
+ schemathesis/runner/impl/core.py,sha256=E38yRz4c5hc_QjZnMSvZwbRYjt6VpoS2KxH2V7l3RIk,39802
74
75
  schemathesis/runner/impl/solo.py,sha256=Y_tNhVBVxcuxv65hN0FjxLlVSC41ebHMOM1xSzVrNk8,3358
75
76
  schemathesis/runner/impl/threadpool.py,sha256=2-2Wvw7msezZtenZY5vU_x3FGLLVlH-ywvhU9hTwAAo,15073
76
77
  schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
@@ -94,7 +95,7 @@ schemathesis/specs/graphql/scalars.py,sha256=W5oj6AcjiXpR-Z6eSSp1oPWl0mjH2NF-w87
94
95
  schemathesis/specs/graphql/schemas.py,sha256=6BlTF9iAongsDxzy4_l85YjDob2QzpB0Vt7mlbIUxaw,12637
95
96
  schemathesis/specs/graphql/validation.py,sha256=SqQbj9uymGUQxlHXc8HkQccIq25uwP5CvLF1zReb1Xg,1636
96
97
  schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
97
- schemathesis/specs/openapi/_hypothesis.py,sha256=o1ssS7JJZ45l750ogoZ3gXoedzLmRsXqpPb1LCZXikY,22835
98
+ schemathesis/specs/openapi/_hypothesis.py,sha256=Qrm6Po2W2wsxZAlLFRUwq0svO8SmQp-5InCFa2cwYaw,23275
98
99
  schemathesis/specs/openapi/checks.py,sha256=1WB_UGNqptfJThWLUNbds1Q-IzOGbbHCOYPIhBYk-zs,5411
99
100
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
100
101
  schemathesis/specs/openapi/converter.py,sha256=9TKeKvNi9MVvoNMfqoPz_cODO8oNrMSTXTOwLLfjD_Q,2799
@@ -106,8 +107,8 @@ schemathesis/specs/openapi/links.py,sha256=FUjqEf6Sv6PeS0UX7CLL5p2VHa9vA1MGDPhx2
106
107
  schemathesis/specs/openapi/loaders.py,sha256=QoEFJ7fxJz5XcGTEWV_ZGXARiektmyyT_xmVtcHco6Q,24131
107
108
  schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
108
109
  schemathesis/specs/openapi/parameters.py,sha256=5jMZQZFsZNBFjG22Bqot-Rc65emiSA4E95rIzwImThk,13610
109
- schemathesis/specs/openapi/references.py,sha256=YGunHAGubYPKbMqQtpFWZm1D4AGxB8wLuiVhE6T6cWo,8978
110
- schemathesis/specs/openapi/schemas.py,sha256=BuBKfToNYIsQ1RIh3EedNaB948-33wLSrjSi1lBV0F0,50457
110
+ schemathesis/specs/openapi/references.py,sha256=zEDzoJbUAwVYTy9CtJ9w1E1Xzp60CVsjebDkFjMXnno,8982
111
+ schemathesis/specs/openapi/schemas.py,sha256=oFRfFucQ3Sf5iZvnVCGJDW0MH7lyA1pSAn7S7bnaSmQ,50466
111
112
  schemathesis/specs/openapi/security.py,sha256=nCUIaZTzI6t26HAfd8YTHW6mFxXAPN9Ds-P9UnXxmNA,6628
112
113
  schemathesis/specs/openapi/serialization.py,sha256=jajqowTIiyEVWEx-Gy4ZinXZewNg0n_ipsGzz7JXP7c,11383
113
114
  schemathesis/specs/openapi/utils.py,sha256=gmW4v6o6sZQifajfPquhFeWmZWGQM89mOOBYZvjnE7g,741
@@ -126,13 +127,13 @@ schemathesis/specs/openapi/stateful/__init__.py,sha256=R8RhPJD3rDzqL4eA9jSnUwh9Q
126
127
  schemathesis/specs/openapi/stateful/links.py,sha256=cSIwho2Hroty6au-gyCD-OwqnuCcIpnIIHU6FvF0SwA,3533
127
128
  schemathesis/stateful/__init__.py,sha256=T7rvhzftfl3wumEsir33DBBzCTK2PtRp9CxBxMLdMSE,4693
128
129
  schemathesis/stateful/state_machine.py,sha256=ZcKpbvEl1QGhVOYnA7Ow6zkuFHtEPDAyCjroPrj-FgU,11343
129
- schemathesis/transports/__init__.py,sha256=YEbD9etlUf6uR_9H8SO01uq2nJ2WBRv30k2pxolOwtE,11203
130
+ schemathesis/transports/__init__.py,sha256=vQ5pyMefhVpbl0FFXtivGsFHWX-7V1PXNwvqCKMJHoI,11423
130
131
  schemathesis/transports/auth.py,sha256=ZKFku9gjhIG6445qNC2p_64Yt9Iz_4azbvja8AMptBk,404
131
132
  schemathesis/transports/content_types.py,sha256=xU8RZWxz-CyWRqQTI2fGYQacB7KasoY7LL_bxPQdyPY,2144
132
133
  schemathesis/transports/headers.py,sha256=EDxpm8su0AuhyqZUkMex-OFZMAJN_5NHah7fDT2HDZE,989
133
134
  schemathesis/transports/responses.py,sha256=j_-udvWbmi6XtEYmpdX8WoFnlrQ6-i3PSBizfozRjQI,1673
134
- schemathesis-3.27.0.dist-info/METADATA,sha256=wVeeZfcbs3AdGnpqcsHGrfx4iYX9gIV8kYgY_HW01Ps,16257
135
- schemathesis-3.27.0.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
136
- schemathesis-3.27.0.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
137
- schemathesis-3.27.0.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
138
- schemathesis-3.27.0.dist-info/RECORD,,
135
+ schemathesis-3.28.0.dist-info/METADATA,sha256=42KfcWW6Hsnb65TECi8kVUb_lTES715x93ei2OjJNgs,17376
136
+ schemathesis-3.28.0.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
137
+ schemathesis-3.28.0.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
138
+ schemathesis-3.28.0.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
139
+ schemathesis-3.28.0.dist-info/RECORD,,