schemathesis 3.34.2__py3-none-any.whl → 3.35.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.
@@ -8,21 +8,21 @@ from typing import Any, Callable, Generator, Mapping, Optional, Tuple
8
8
 
9
9
  import hypothesis
10
10
  from hypothesis import Phase
11
- from hypothesis import strategies as st
12
11
  from hypothesis.errors import HypothesisWarning, Unsatisfiable
13
12
  from hypothesis.internal.entropy import deterministic_PRNG
14
13
  from hypothesis.internal.reflection import proxies
15
14
  from jsonschema.exceptions import SchemaError
16
15
 
17
16
  from .auths import get_auth_storage_from_test
18
- from .constants import DEFAULT_DEADLINE
17
+ from .constants import DEFAULT_DEADLINE, NOT_SET
19
18
  from .exceptions import OperationSchemaError, SerializationNotPossible
20
- from .generation import DataGenerationMethod, GenerationConfig
19
+ from .experimental import COVERAGE_PHASE
20
+ from .generation import DataGenerationMethod, GenerationConfig, combine_strategies, coverage, get_single_example
21
21
  from .hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
22
22
  from .models import APIOperation, Case
23
23
  from .transports.content_types import parse_content_type
24
24
  from .transports.headers import has_invalid_characters, is_latin_1_encodable
25
- from .utils import GivenInput, combine_strategies
25
+ from .utils import GivenInput
26
26
 
27
27
  # Forcefully initializes Hypothesis' global PRNG to avoid races that initilize it
28
28
  # if e.g. Schemathesis CLI is used with multiple workers
@@ -101,6 +101,8 @@ def create_test(
101
101
  wrapped_test = add_examples(
102
102
  wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
103
103
  )
104
+ if COVERAGE_PHASE.is_enabled:
105
+ wrapped_test = add_coverage(wrapped_test, operation, data_generation_methods)
104
106
  return wrapped_test
105
107
 
106
108
 
@@ -199,6 +201,55 @@ def add_examples(
199
201
  return test
200
202
 
201
203
 
204
+ def add_coverage(
205
+ test: Callable, operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
206
+ ) -> Callable:
207
+ for example in _iter_coverage_cases(operation, data_generation_methods):
208
+ test = hypothesis.example(case=example)(test)
209
+ return test
210
+
211
+
212
+ def _iter_coverage_cases(
213
+ operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
214
+ ) -> Generator[Case, None, None]:
215
+ from .specs.openapi.constants import LOCATION_TO_CONTAINER
216
+
217
+ ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
218
+ generators: dict[tuple[str, str], Generator] = {}
219
+ template: dict[str, Any] = {}
220
+ for parameter in operation.iter_parameters():
221
+ schema = parameter.as_json_schema(operation)
222
+ gen = coverage.cover_schema_iter(ctx, schema)
223
+ value = next(gen, NOT_SET)
224
+ if value is NOT_SET:
225
+ continue
226
+ location = parameter.location
227
+ name = parameter.name
228
+ container = template.setdefault(LOCATION_TO_CONTAINER[location], {})
229
+ container[name] = value
230
+ generators[(location, name)] = gen
231
+ if operation.body:
232
+ for body in operation.body:
233
+ schema = body.as_json_schema(operation)
234
+ gen = coverage.cover_schema_iter(ctx, schema)
235
+ value = next(gen, NOT_SET)
236
+ if value is NOT_SET:
237
+ continue
238
+ if "body" not in template:
239
+ template["body"] = value
240
+ template["media_type"] = body.media_type
241
+ yield operation.make_case(**{**template, "body": value, "media_type": body.media_type})
242
+ for next_value in gen:
243
+ yield operation.make_case(**{**template, "body": next_value, "media_type": body.media_type})
244
+ else:
245
+ yield operation.make_case(**template)
246
+ for (location, name), gen in generators.items():
247
+ container_name = LOCATION_TO_CONTAINER[location]
248
+ container = template[container_name]
249
+ for value in gen:
250
+ yield operation.make_case(**{**template, container_name: {**container, name: value}})
251
+
252
+
202
253
  def find_invalid_headers(headers: Mapping) -> Generator[Tuple[str, str], None, None]:
203
254
  for name, value in headers.items():
204
255
  if not is_latin_1_encodable(value) or has_invalid_characters(name, value):
@@ -248,25 +299,3 @@ def get_invalid_example_headers_mark(test: Callable) -> Optional[dict[str, str]]
248
299
 
249
300
  def add_invalid_example_header_mark(test: Callable, headers: dict[str, str]) -> None:
250
301
  test._schemathesis_invalid_example_headers = headers # type: ignore
251
-
252
-
253
- def get_single_example(strategy: st.SearchStrategy[Case]) -> Case:
254
- examples: list[Case] = []
255
- add_single_example(strategy, examples)
256
- return examples[0]
257
-
258
-
259
- def add_single_example(strategy: st.SearchStrategy[Case], examples: list[Case]) -> None:
260
- @hypothesis.given(strategy) # type: ignore
261
- @hypothesis.settings( # type: ignore
262
- database=None,
263
- max_examples=1,
264
- deadline=None,
265
- verbosity=hypothesis.Verbosity.quiet,
266
- phases=(hypothesis.Phase.generate,),
267
- suppress_health_check=list(hypothesis.HealthCheck),
268
- )
269
- def example_generating_inner_function(ex: Case) -> None:
270
- examples.append(ex)
271
-
272
- example_generating_inner_function()
@@ -11,7 +11,7 @@ from collections import defaultdict
11
11
  from dataclasses import dataclass
12
12
  from enum import Enum
13
13
  from queue import Queue
14
- from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence, cast
14
+ from typing import TYPE_CHECKING, Any, Callable, Generator, Iterable, Literal, NoReturn, Sequence, Type, cast
15
15
  from urllib.parse import urlparse
16
16
 
17
17
  import click
@@ -42,7 +42,7 @@ from ..internal.datetime import current_datetime
42
42
  from ..internal.output import OutputConfig
43
43
  from ..internal.validation import file_exists
44
44
  from ..loaders import load_app, load_yaml
45
- from ..models import APIOperation, Case, CheckFunction
45
+ from ..models import Case, CheckFunction
46
46
  from ..runner import events, prepare_hypothesis_settings, probes
47
47
  from ..specs.graphql import loaders as gql_loaders
48
48
  from ..specs.openapi import loaders as oas_loaders
@@ -50,11 +50,12 @@ from ..stateful import Stateful
50
50
  from ..targets import Target
51
51
  from ..transports import RequestConfig
52
52
  from ..transports.auth import get_requests_auth
53
- from ..types import Filter, PathLike, RequestCert
53
+ from ..types import PathLike, RequestCert
54
54
  from . import callbacks, cassettes, output
55
55
  from .constants import DEFAULT_WORKERS, MAX_WORKERS, MIN_WORKERS, HealthCheck, Phase, Verbosity
56
56
  from .context import ExecutionContext, FileReportContext, ServiceReportContext
57
57
  from .debug import DebugOutputHandler
58
+ from .handlers import EventHandler
58
59
  from .junitxml import JunitXMLHandler
59
60
  from .options import CsvChoice, CsvEnumChoice, CustomHelpMessageChoice, NotSet, OptionalInt
60
61
  from .sanitization import SanitizationHandler
@@ -66,13 +67,13 @@ if TYPE_CHECKING:
66
67
  from ..schemas import BaseSchema
67
68
  from ..service.client import ServiceClient
68
69
  from ..specs.graphql.schemas import GraphQLSchema
69
- from .handlers import EventHandler
70
70
 
71
71
 
72
72
  def _get_callable_names(items: tuple[Callable, ...]) -> tuple[str, ...]:
73
73
  return tuple(item.__name__ for item in items)
74
74
 
75
75
 
76
+ CUSTOM_HANDLERS: list[Type[EventHandler]] = []
76
77
  CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
77
78
 
78
79
  DEFAULT_CHECKS_NAMES = _get_callable_names(checks_module.DEFAULT_CHECKS)
@@ -759,6 +760,7 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
759
760
  experimental.SCHEMA_ANALYSIS.name,
760
761
  experimental.STATEFUL_TEST_RUNNER.name,
761
762
  experimental.STATEFUL_ONLY.name,
763
+ experimental.COVERAGE_PHASE.name,
762
764
  ]
763
765
  ),
764
766
  callback=callbacks.convert_experimental,
@@ -927,6 +929,7 @@ def run(
927
929
  schemathesis_io_telemetry: bool = True,
928
930
  hosts_file: PathLike = service.DEFAULT_HOSTS_PATH,
929
931
  force_color: bool = False,
932
+ **__kwargs,
930
933
  ) -> None:
931
934
  """Run tests against an API using a specified SCHEMA.
932
935
 
@@ -1212,6 +1215,7 @@ def run(
1212
1215
  )
1213
1216
  execute(
1214
1217
  event_stream,
1218
+ ctx=ctx,
1215
1219
  hypothesis_settings=hypothesis_settings,
1216
1220
  workers_num=workers_num,
1217
1221
  rate_limit=rate_limit,
@@ -1595,6 +1599,7 @@ class OutputStyle(Enum):
1595
1599
  def execute(
1596
1600
  event_stream: Generator[events.ExecutionEvent, None, None],
1597
1601
  *,
1602
+ ctx: click.Context,
1598
1603
  hypothesis_settings: hypothesis.settings,
1599
1604
  workers_num: int,
1600
1605
  rate_limit: str | None,
@@ -1669,6 +1674,8 @@ def execute(
1669
1674
  cassette_path, format=cassette_format, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes
1670
1675
  )
1671
1676
  )
1677
+ for custom_handler in CUSTOM_HANDLERS:
1678
+ handlers.append(custom_handler(*ctx.args, **ctx.params))
1672
1679
  handlers.append(get_output_handler(workers_num))
1673
1680
  if sanitize_output:
1674
1681
  handlers.insert(0, SanitizationHandler())
@@ -2000,6 +2007,20 @@ def decide_color_output(ctx: click.Context, no_color: bool, force_color: bool) -
2000
2007
  ctx.color = False
2001
2008
 
2002
2009
 
2010
+ def add_option(*args: Any, cls: Type = click.Option, **kwargs: Any) -> None:
2011
+ """Add a new CLI option to `st run`."""
2012
+ run.params.append(cls(args, **kwargs))
2013
+
2014
+
2015
+ def handler() -> Callable[[Type], None]:
2016
+ """Register a new CLI event handler."""
2017
+
2018
+ def _wrapper(cls: Type) -> None:
2019
+ CUSTOM_HANDLERS.append(cls)
2020
+
2021
+ return _wrapper
2022
+
2023
+
2003
2024
  @HookDispatcher.register_spec([HookScope.GLOBAL])
2004
2025
  def after_init_cli_run_handlers(
2005
2026
  context: HookContext, handlers: list[EventHandler], execution_context: ExecutionContext
@@ -60,7 +60,11 @@ class ExecutionContext:
60
60
  analysis: Result[AnalysisResult, Exception] | None = None
61
61
  output_config: OutputConfig = field(default_factory=OutputConfig)
62
62
  state_machine_sink: StateMachineSink | None = None
63
+ summary_lines: list[str] = field(default_factory=list)
63
64
 
64
65
  @deprecated_property(removed_in="4.0", replacement="show_trace")
65
66
  def show_errors_tracebacks(self) -> bool:
66
67
  return self.show_trace
68
+
69
+ def add_summary_line(self, line: str) -> None:
70
+ self.summary_lines.append(line)
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Any
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from ..runner import events
@@ -8,6 +8,9 @@ if TYPE_CHECKING:
8
8
 
9
9
 
10
10
  class EventHandler:
11
+ def __init__(self, *args: Any, **params: Any) -> None:
12
+ pass
13
+
11
14
  def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
12
15
  raise NotImplementedError
13
16
 
@@ -850,6 +850,10 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
850
850
  display_application_logs(context, event)
851
851
  display_analysis(context)
852
852
  display_statistic(context, event)
853
+ if context.summary_lines:
854
+ click.echo()
855
+ for line in context.summary_lines:
856
+ click.echo(line)
853
857
  click.echo()
854
858
  display_summary(event)
855
859
 
@@ -18,7 +18,7 @@ def uninstall() -> None:
18
18
 
19
19
  def before_add_examples(context: HookContext, examples: list[Case]) -> None:
20
20
  if not examples and context.operation is not None:
21
- from ..._hypothesis import add_single_example
21
+ from ...generation import add_single_example
22
22
 
23
23
  strategy = context.operation.as_strategy()
24
24
  add_single_example(strategy, examples)
@@ -93,3 +93,10 @@ STATEFUL_ONLY = GLOBAL_EXPERIMENTS.create_experiment(
93
93
  description="Run only stateful tests",
94
94
  discussion_url="https://github.com/schemathesis/schemathesis/discussions/2262",
95
95
  )
96
+ COVERAGE_PHASE = GLOBAL_EXPERIMENTS.create_experiment(
97
+ name="coverage-phase",
98
+ verbose_name="Coverage phase",
99
+ env_var="COVERAGE_PHASE",
100
+ description="Generate covering test cases",
101
+ discussion_url="https://github.com/schemathesis/schemathesis/discussions/2418",
102
+ )
@@ -2,48 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  import random
4
4
  from dataclasses import dataclass, field
5
- from enum import Enum
6
- from typing import TYPE_CHECKING, Iterable, Union
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ._hypothesis import add_single_example, combine_strategies, get_single_example # noqa: E402
8
+ from ._methods import DataGenerationMethod, DataGenerationMethodInput # noqa: E402
7
9
 
8
10
  if TYPE_CHECKING:
9
11
  from hypothesis.strategies import SearchStrategy
10
12
 
11
13
 
12
- class DataGenerationMethod(str, Enum):
13
- """Defines what data Schemathesis generates for tests."""
14
-
15
- # Generate data, that fits the API schema
16
- positive = "positive"
17
- # Doesn't fit the API schema
18
- negative = "negative"
19
-
20
- @classmethod
21
- def default(cls) -> DataGenerationMethod:
22
- return cls.positive
23
-
24
- @classmethod
25
- def all(cls) -> list[DataGenerationMethod]:
26
- return list(DataGenerationMethod)
27
-
28
- def as_short_name(self) -> str:
29
- return {
30
- DataGenerationMethod.positive: "P",
31
- DataGenerationMethod.negative: "N",
32
- }[self]
33
-
34
- @property
35
- def is_negative(self) -> bool:
36
- return self == DataGenerationMethod.negative
37
-
38
- @classmethod
39
- def ensure_list(cls, value: DataGenerationMethodInput) -> list[DataGenerationMethod]:
40
- if isinstance(value, DataGenerationMethod):
41
- return [value]
42
- return list(value)
43
-
44
-
45
- DataGenerationMethodInput = Union[DataGenerationMethod, Iterable[DataGenerationMethod]]
46
-
47
14
  DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
48
15
 
49
16
 
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import lru_cache, reduce
4
+ from operator import or_
5
+ from typing import TYPE_CHECKING, TypeVar
6
+
7
+ if TYPE_CHECKING:
8
+ from hypothesis import settings
9
+ from hypothesis import strategies as st
10
+
11
+
12
+ @lru_cache()
13
+ def default_settings() -> settings:
14
+ from hypothesis import HealthCheck, Phase, Verbosity, settings
15
+
16
+ return settings(
17
+ database=None,
18
+ max_examples=1,
19
+ deadline=None,
20
+ verbosity=Verbosity.quiet,
21
+ phases=(Phase.generate,),
22
+ suppress_health_check=list(HealthCheck),
23
+ )
24
+
25
+
26
+ T = TypeVar("T")
27
+
28
+
29
+ def get_single_example(strategy: st.SearchStrategy[T]) -> T: # type: ignore[type-var]
30
+ examples: list[T] = []
31
+ add_single_example(strategy, examples)
32
+ return examples[0]
33
+
34
+
35
+ def add_single_example(strategy: st.SearchStrategy[T], examples: list[T]) -> None:
36
+ from hypothesis import given
37
+
38
+ @given(strategy) # type: ignore
39
+ @default_settings() # type: ignore
40
+ def example_generating_inner_function(ex: T) -> None:
41
+ examples.append(ex)
42
+
43
+ example_generating_inner_function()
44
+
45
+
46
+ def combine_strategies(strategies: list[st.SearchStrategy] | tuple[st.SearchStrategy]) -> st.SearchStrategy:
47
+ """Combine a list of strategies into a single one.
48
+
49
+ If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
50
+ """
51
+ return reduce(or_, strategies[1:], strategies[0])
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import Iterable, Union
5
+
6
+
7
+ class DataGenerationMethod(str, Enum):
8
+ """Defines what data Schemathesis generates for tests."""
9
+
10
+ # Generate data, that fits the API schema
11
+ positive = "positive"
12
+ # Doesn't fit the API schema
13
+ negative = "negative"
14
+
15
+ @classmethod
16
+ def default(cls) -> DataGenerationMethod:
17
+ return cls.positive
18
+
19
+ @classmethod
20
+ def all(cls) -> list[DataGenerationMethod]:
21
+ return list(DataGenerationMethod)
22
+
23
+ def as_short_name(self) -> str:
24
+ return {
25
+ DataGenerationMethod.positive: "P",
26
+ DataGenerationMethod.negative: "N",
27
+ }[self]
28
+
29
+ @property
30
+ def is_negative(self) -> bool:
31
+ return self == DataGenerationMethod.negative
32
+
33
+ @classmethod
34
+ def ensure_list(cls, value: DataGenerationMethodInput) -> list[DataGenerationMethod]:
35
+ if isinstance(value, DataGenerationMethod):
36
+ return [value]
37
+ return list(value)
38
+
39
+
40
+ DataGenerationMethodInput = Union[DataGenerationMethod, Iterable[DataGenerationMethod]]
@@ -0,0 +1,433 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from contextlib import suppress
5
+ from dataclasses import dataclass, field
6
+ from functools import lru_cache
7
+ from typing import Any, Generator, Set, TypeVar, cast
8
+
9
+ import jsonschema
10
+ from hypothesis import strategies as st
11
+ from hypothesis.errors import Unsatisfiable
12
+ from hypothesis_jsonschema import from_schema
13
+ from hypothesis_jsonschema._canonicalise import canonicalish
14
+
15
+ from ._hypothesis import combine_strategies, get_single_example
16
+ from ._methods import DataGenerationMethod
17
+
18
+ BUFFER_SIZE = 8 * 1024
19
+ FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(lambda x: x or 0.0)
20
+ NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
21
+ JSON_STRATEGY: st.SearchStrategy = st.recursive(
22
+ st.none() | st.booleans() | NUMERIC_STRATEGY | st.text(),
23
+ lambda strategy: st.lists(strategy, max_size=3) | st.dictionaries(st.text(), strategy, max_size=3),
24
+ )
25
+ ARRAY_STRATEGY: st.SearchStrategy = st.lists(JSON_STRATEGY)
26
+ OBJECT_STRATEGY: st.SearchStrategy = st.dictionaries(st.text(), JSON_STRATEGY)
27
+
28
+ UNKNOWN_PROPERTY_KEY = "x-schemathesis-unknown-property"
29
+ UNKNOWN_PROPERTY_VALUE = 42
30
+
31
+
32
+ @lru_cache(maxsize=128)
33
+ def cached_draw(strategy: st.SearchStrategy) -> Any:
34
+ return get_single_example(strategy)
35
+
36
+
37
+ @dataclass
38
+ class CoverageContext:
39
+ data_generation_methods: list[DataGenerationMethod] = field(default_factory=DataGenerationMethod.all)
40
+
41
+ @classmethod
42
+ def with_positive(cls) -> CoverageContext:
43
+ return CoverageContext(data_generation_methods=[DataGenerationMethod.positive])
44
+
45
+ @classmethod
46
+ def with_negative(cls) -> CoverageContext:
47
+ return CoverageContext(data_generation_methods=[DataGenerationMethod.negative])
48
+
49
+ def generate_from(self, strategy: st.SearchStrategy, cached: bool = False) -> Any:
50
+ if cached:
51
+ value = cached_draw(strategy)
52
+ else:
53
+ value = get_single_example(strategy)
54
+ return value
55
+
56
+ def generate_from_schema(self, schema: dict) -> Any:
57
+ return self.generate_from(from_schema(schema))
58
+
59
+
60
+ T = TypeVar("T")
61
+
62
+
63
+ def _to_hashable_key(value: T) -> T | tuple[type, str]:
64
+ if isinstance(value, (dict, list)):
65
+ serialized = json.dumps(value, sort_keys=True)
66
+ return (type(value), serialized)
67
+ return value
68
+
69
+
70
+ def cover_schema(ctx: CoverageContext, schema: dict) -> list:
71
+ return list(cover_schema_iter(ctx, schema))
72
+
73
+
74
+ def _cover_positive_for_type(ctx: CoverageContext, schema: dict, ty: str | None) -> Generator:
75
+ if ty == "object" or ty == "array":
76
+ template_schema = _get_template_schema(schema, ty)
77
+ template = ctx.generate_from_schema(template_schema)
78
+ else:
79
+ template = None
80
+ if DataGenerationMethod.positive in ctx.data_generation_methods:
81
+ ctx = ctx.with_positive()
82
+ enum = schema.get("enum")
83
+ const = schema.get("const")
84
+ for key in ("anyOf", "oneOf"):
85
+ sub_schemas = schema.get(key)
86
+ if sub_schemas is not None:
87
+ for sub_schema in sub_schemas:
88
+ yield from cover_schema_iter(ctx, sub_schema)
89
+ all_of = schema.get("allOf")
90
+ if all_of is not None:
91
+ if len(all_of) == 1:
92
+ yield from cover_schema_iter(ctx, all_of[0])
93
+ else:
94
+ canonical = canonicalish(schema)
95
+ yield from cover_schema_iter(ctx, canonical)
96
+ if enum is not None:
97
+ yield from enum
98
+ elif const is not None:
99
+ yield const
100
+ elif ty is not None:
101
+ if ty == "null":
102
+ yield None
103
+ if ty == "boolean":
104
+ yield True
105
+ yield False
106
+ if ty == "string":
107
+ yield from _positive_string(ctx, schema)
108
+ if ty == "integer" or ty == "number":
109
+ yield from _positive_number(ctx, schema)
110
+ if ty == "array":
111
+ yield from _positive_array(ctx, schema, cast(list, template))
112
+ if ty == "object":
113
+ yield from _positive_object(ctx, schema, cast(dict, template))
114
+
115
+
116
+ def cover_schema_iter(ctx: CoverageContext, schema: dict) -> Generator:
117
+ types = schema.get("type", [])
118
+ if not isinstance(types, list):
119
+ types = [types]
120
+ if not types:
121
+ with suppress(Unsatisfiable, jsonschema.RefResolutionError):
122
+ yield from _cover_positive_for_type(ctx, schema, None)
123
+ for ty in types:
124
+ with suppress(Unsatisfiable, jsonschema.RefResolutionError):
125
+ yield from _cover_positive_for_type(ctx, schema, ty)
126
+ if DataGenerationMethod.negative in ctx.data_generation_methods:
127
+ template = None
128
+ seen: Set[Any | tuple[type, str]] = set()
129
+ for key, value in schema.items():
130
+ with suppress(Unsatisfiable, jsonschema.RefResolutionError):
131
+ if key == "enum":
132
+ yield from _negative_enum(ctx, value)
133
+ elif key == "const":
134
+ yield from _negative_enum(ctx, [value])
135
+ elif key == "type":
136
+ yield from _negative_type(ctx, seen, value)
137
+ elif key == "properties":
138
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
139
+ yield from _negative_properties(ctx, template, value)
140
+ elif key == "pattern":
141
+ yield from _negative_pattern(ctx, value)
142
+ elif key == "format" and ("string" in types or not types):
143
+ yield from _negative_format(ctx, schema, value)
144
+ elif key == "maximum":
145
+ next = value + 1
146
+ yield next
147
+ seen.add(next)
148
+ elif key == "minimum":
149
+ next = value - 1
150
+ yield next
151
+ seen.add(next)
152
+ elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
153
+ yield value
154
+ seen.add(value)
155
+ elif key == "multipleOf":
156
+ yield from _negative_multiple_of(ctx, schema, value)
157
+ elif key == "minLength" and 0 < value < BUFFER_SIZE and "pattern" not in schema:
158
+ yield ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
159
+ elif key == "maxLength" and value < BUFFER_SIZE and "pattern" not in schema:
160
+ yield ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
161
+ elif key == "uniqueItems" and value:
162
+ yield from _negative_unique_items(ctx, schema)
163
+ elif key == "required":
164
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
165
+ yield from _negative_required(ctx, template, value)
166
+ elif key == "additionalProperties" and not value:
167
+ template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
168
+ yield {**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE}
169
+ elif key == "allOf":
170
+ nctx = ctx.with_negative()
171
+ if len(value) == 1:
172
+ yield from cover_schema_iter(nctx, value[0])
173
+ else:
174
+ canonical = canonicalish(schema)
175
+ yield from cover_schema_iter(nctx, canonical)
176
+ elif key == "anyOf" or key == "oneOf":
177
+ nctx = ctx.with_negative()
178
+ # NOTE: Other sub-schemas are not filtered out
179
+ for sub_schema in value:
180
+ yield from cover_schema_iter(nctx, sub_schema)
181
+
182
+
183
+ def _get_template_schema(schema: dict, ty: str) -> dict:
184
+ if ty == "object":
185
+ properties = schema.get("properties")
186
+ if properties is not None:
187
+ return {
188
+ **schema,
189
+ "required": list(properties),
190
+ "type": ty,
191
+ "properties": {
192
+ k: _get_template_schema(v, "object") if v.get("type") == "object" else v
193
+ for k, v in properties.items()
194
+ },
195
+ }
196
+ return {**schema, "type": ty}
197
+
198
+
199
+ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator:
200
+ """Generate positive string values."""
201
+ # Boundary and near boundary values
202
+ min_length = schema.get("minLength")
203
+ max_length = schema.get("maxLength")
204
+
205
+ if not min_length and not max_length:
206
+ # Default positive value
207
+ yield ctx.generate_from_schema(schema)
208
+
209
+ seen = set()
210
+
211
+ if min_length is not None and min_length < BUFFER_SIZE and "pattern" not in schema:
212
+ # Exactly the minimum length
213
+ yield ctx.generate_from_schema({**schema, "maxLength": min_length})
214
+ seen.add(min_length)
215
+
216
+ # One character more than minimum if possible
217
+ larger = min_length + 1
218
+ if larger < BUFFER_SIZE and larger not in seen and (not max_length or larger <= max_length):
219
+ yield ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger})
220
+ seen.add(larger)
221
+
222
+ if max_length is not None and "pattern" not in schema:
223
+ # Exactly the maximum length
224
+ if max_length < BUFFER_SIZE and max_length not in seen:
225
+ yield ctx.generate_from_schema({**schema, "minLength": max_length})
226
+ seen.add(max_length)
227
+
228
+ # One character less than maximum if possible
229
+ smaller = max_length - 1
230
+ if (
231
+ smaller < BUFFER_SIZE
232
+ and smaller not in seen
233
+ and (smaller > 0 and (min_length is None or smaller >= min_length))
234
+ ):
235
+ yield ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller})
236
+ seen.add(smaller)
237
+
238
+
239
+ def closest_multiple_greater_than(y: int, x: int) -> int:
240
+ """Find the closest multiple of X that is greater than Y."""
241
+ quotient, remainder = divmod(y, x)
242
+ if remainder == 0:
243
+ return y
244
+ return x * (quotient + 1)
245
+
246
+
247
+ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator:
248
+ """Generate positive integer values."""
249
+ # Boundary and near boundary values
250
+ minimum = schema.get("minimum")
251
+ maximum = schema.get("maximum")
252
+ exclusive_minimum = schema.get("exclusiveMinimum")
253
+ exclusive_maximum = schema.get("exclusiveMaximum")
254
+ if exclusive_minimum is not None:
255
+ minimum = exclusive_minimum + 1
256
+ if exclusive_maximum is not None:
257
+ maximum = exclusive_maximum - 1
258
+ multiple_of = schema.get("multipleOf")
259
+
260
+ if not minimum and not maximum:
261
+ # Default positive value
262
+ yield ctx.generate_from_schema(schema)
263
+
264
+ seen = set()
265
+
266
+ if minimum is not None:
267
+ # Exactly the minimum
268
+ if multiple_of is not None:
269
+ smallest = closest_multiple_greater_than(minimum, multiple_of)
270
+ else:
271
+ smallest = minimum
272
+ seen.add(smallest)
273
+ yield smallest
274
+
275
+ # One more than minimum if possible
276
+ if multiple_of is not None:
277
+ larger = smallest + multiple_of
278
+ else:
279
+ larger = minimum + 1
280
+ if larger not in seen and (not maximum or larger <= maximum):
281
+ seen.add(larger)
282
+ yield larger
283
+
284
+ if maximum is not None:
285
+ # Exactly the maximum
286
+ if multiple_of is not None:
287
+ largest = maximum - (maximum % multiple_of)
288
+ else:
289
+ largest = maximum
290
+ if largest not in seen:
291
+ seen.add(largest)
292
+ yield largest
293
+
294
+ # One less than maximum if possible
295
+ if multiple_of is not None:
296
+ smaller = largest - multiple_of
297
+ else:
298
+ smaller = maximum - 1
299
+ if smaller not in seen and (smaller > 0 and (minimum is None or smaller >= minimum)):
300
+ seen.add(smaller)
301
+ yield smaller
302
+
303
+
304
+ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator:
305
+ seen = set()
306
+ yield template
307
+ seen.add(len(template))
308
+
309
+ # Boundary and near-boundary sizes
310
+ min_items = schema.get("minItems")
311
+ max_items = schema.get("maxItems")
312
+ if min_items is not None:
313
+ # Do not generate an array with `minItems` length, because it is already covered by `template`
314
+
315
+ # One item more than minimum if possible
316
+ larger = min_items + 1
317
+ if larger not in seen and (max_items is None or larger <= max_items):
318
+ yield ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
319
+ seen.add(larger)
320
+
321
+ if max_items is not None:
322
+ if max_items not in seen:
323
+ yield ctx.generate_from_schema({**schema, "minItems": max_items})
324
+ seen.add(max_items)
325
+
326
+ # One item smaller than maximum if possible
327
+ smaller = max_items - 1
328
+ if smaller > 0 and smaller not in seen and (min_items is None or smaller >= min_items):
329
+ yield ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
330
+ seen.add(smaller)
331
+
332
+
333
+ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator:
334
+ yield template
335
+ # Only required properties
336
+ properties = schema.get("properties", {})
337
+ if set(properties) != set(schema.get("required", {})):
338
+ only_required = {k: v for k, v in template.items() if k in schema.get("required", [])}
339
+ yield only_required
340
+ seen = set()
341
+ for name, sub_schema in properties.items():
342
+ seen.add(_to_hashable_key(template.get(name)))
343
+ for new in cover_schema_iter(ctx, sub_schema):
344
+ key = _to_hashable_key(new)
345
+ if key not in seen:
346
+ yield {**template, name: new}
347
+ seen.add(key)
348
+ seen.clear()
349
+
350
+
351
+ @lru_cache(maxsize=128)
352
+ def _get_negative_enum_strategy(value: tuple) -> st.SearchStrategy:
353
+ return JSON_STRATEGY.filter(lambda x: x not in value)
354
+
355
+
356
+ def _negative_enum(ctx: CoverageContext, value: list) -> Generator:
357
+ try:
358
+ strategy = _get_negative_enum_strategy(tuple(value))
359
+ except TypeError:
360
+ # The value is not hashable
361
+ strategy = JSON_STRATEGY.filter(lambda x: x not in value)
362
+ # The exact negative value is not important here
363
+ yield ctx.generate_from(strategy, cached=True)
364
+
365
+
366
+ def _negative_properties(ctx: CoverageContext, template: dict, properties: dict) -> Generator:
367
+ nctx = ctx.with_negative()
368
+ for key, sub_schema in properties.items():
369
+ for value in cover_schema_iter(nctx, sub_schema):
370
+ yield {**template, key: value}
371
+
372
+
373
+ @lru_cache(maxsize=128)
374
+ def _get_negative_pattern_strategy(value: str) -> st.SearchStrategy:
375
+ return st.text().filter(lambda x: x != value)
376
+
377
+
378
+ def _negative_pattern(ctx: CoverageContext, pattern: str) -> Generator:
379
+ yield ctx.generate_from(_get_negative_pattern_strategy(pattern), cached=True)
380
+
381
+
382
+ def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
383
+ return {"allOf": [{k: v for k, v in schema.items() if k != key}, {"not": {key: value}}]}
384
+
385
+
386
+ def _negative_multiple_of(ctx: CoverageContext, schema: dict, multiple_of: int | float) -> Generator:
387
+ yield ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of))
388
+
389
+
390
+ def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator:
391
+ unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
392
+ yield unique + unique
393
+
394
+
395
+ def _negative_required(ctx: CoverageContext, template: dict, required: list[str]) -> Generator:
396
+ for key in required:
397
+ yield {k: v for k, v in template.items() if k != key}
398
+
399
+
400
+ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator:
401
+ # Hypothesis-jsonschema does not canonicalise it properly right now, which leads to unsatisfiable schema
402
+ without_format = {k: v for k, v in schema.items() if k != "format"}
403
+ without_format.setdefault("type", "string")
404
+ strategy = from_schema(without_format)
405
+ if format in jsonschema.Draft202012Validator.FORMAT_CHECKER.checkers:
406
+ strategy = strategy.filter(lambda v: not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format))
407
+ yield ctx.generate_from(strategy)
408
+
409
+
410
+ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator:
411
+ strategies = {
412
+ "integer": st.integers(),
413
+ "number": NUMERIC_STRATEGY,
414
+ "boolean": st.booleans(),
415
+ "null": st.none(),
416
+ "string": st.text(),
417
+ "array": ARRAY_STRATEGY,
418
+ "object": OBJECT_STRATEGY,
419
+ }
420
+ if isinstance(ty, str):
421
+ types = [ty]
422
+ else:
423
+ types = ty
424
+ for ty_ in types:
425
+ strategies.pop(ty_)
426
+ if "number" in types:
427
+ del strategies["integer"]
428
+ if "integer" in types:
429
+ strategies["number"] = FLOAT_STRATEGY.filter(lambda x: x != int(x))
430
+ negative_strategy = combine_strategies(tuple(strategies.values())).filter(lambda x: _to_hashable_key(x) not in seen)
431
+ value = ctx.generate_from(negative_strategy, cached=True)
432
+ yield value
433
+ seen.add(_to_hashable_key(value))
schemathesis/schemas.py CHANGED
@@ -50,6 +50,7 @@ from .generation import (
50
50
  DataGenerationMethod,
51
51
  DataGenerationMethodInput,
52
52
  GenerationConfig,
53
+ combine_strategies,
53
54
  )
54
55
  from .hooks import HookContext, HookDispatcher, HookScope, dispatch, to_filterable_hook
55
56
  from .internal.deprecation import warn_filtration_arguments
@@ -69,7 +70,7 @@ from .types import (
69
70
  PathParameters,
70
71
  Query,
71
72
  )
72
- from .utils import PARAMETRIZE_MARKER, GivenInput, combine_strategies, given_proxy
73
+ from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy
73
74
 
74
75
  if TYPE_CHECKING:
75
76
  from .transports import Transport
@@ -128,7 +128,7 @@ def _apply_schema_patches_extension(extension: SchemaPatchesExtension, schema: B
128
128
 
129
129
 
130
130
  def strategy_from_definitions(definitions: list[StrategyDefinition]) -> Result[st.SearchStrategy, Exception]:
131
- from ..utils import combine_strategies
131
+ from ..generation import combine_strategies
132
132
 
133
133
  strategies = []
134
134
  for definition in definitions:
@@ -10,8 +10,8 @@ import requests
10
10
  from hypothesis.strategies import SearchStrategy
11
11
  from hypothesis_jsonschema import from_schema
12
12
 
13
- from ..._hypothesis import get_single_example
14
13
  from ...constants import DEFAULT_RESPONSE_TIMEOUT
14
+ from ...generation import get_single_example
15
15
  from ...internal.copy import fast_deepcopy
16
16
  from ...models import APIOperation, Case
17
17
  from ._hypothesis import get_case_strategy, get_default_format_strategies
@@ -1032,6 +1032,7 @@ class SwaggerV20(BaseOpenAPISchema):
1032
1032
  query: Query | None = None,
1033
1033
  body: Body | NotSet = NOT_SET,
1034
1034
  media_type: str | None = None,
1035
+ generation_time: float = 0.0,
1035
1036
  ) -> C:
1036
1037
  if body is not NOT_SET and media_type is None:
1037
1038
  media_type = operation._get_default_media_type()
@@ -1043,7 +1044,7 @@ class SwaggerV20(BaseOpenAPISchema):
1043
1044
  query=query,
1044
1045
  body=body,
1045
1046
  media_type=media_type,
1046
- generation_time=0.0,
1047
+ generation_time=generation_time,
1047
1048
  )
1048
1049
 
1049
1050
  def _get_consumes_for_operation(self, definition: dict[str, Any]) -> list[str]:
@@ -8,10 +8,10 @@ from hypothesis import strategies as st
8
8
  from hypothesis.stateful import Bundle, Rule, precondition, rule
9
9
 
10
10
  from ....constants import NOT_SET
11
+ from ....generation import DataGenerationMethod, combine_strategies
11
12
  from ....internal.result import Ok
12
13
  from ....stateful.state_machine import APIStateMachine, Direction, StepResult
13
14
  from ....types import NotSet
14
- from ....utils import combine_strategies
15
15
  from .. import expressions
16
16
  from ..links import get_all_links
17
17
  from ..utils import expand_status_code
@@ -43,6 +43,10 @@ class OpenAPIStateMachine(APIStateMachine):
43
43
  return "\n".join(item.line for item in cls._transition_stats_template.iter_with_format())
44
44
 
45
45
 
46
+ # The proportion of negative tests generated for "root" transitions
47
+ NEGATIVE_TEST_CASES_THRESHOLD = 20
48
+
49
+
46
50
  def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
47
51
  """Create a state machine class.
48
52
 
@@ -111,12 +115,24 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
111
115
  # The source operation has no prerequisite, but we need to allow this rule to be executed
112
116
  # in order to reach other transitions
113
117
  name = _normalize_name(f"{target.verbose_name} -> X")
114
- case_strategy = combine_strategies(
115
- [
116
- target.as_strategy(data_generation_method=data_generation_method)
117
- for data_generation_method in schema.data_generation_methods
118
- ]
119
- )
118
+ if len(schema.data_generation_methods) == 1:
119
+ case_strategy = target.as_strategy(data_generation_method=schema.data_generation_methods[0])
120
+ else:
121
+ strategies = {
122
+ method: target.as_strategy(data_generation_method=method)
123
+ for method in schema.data_generation_methods
124
+ }
125
+
126
+ @st.composite # type: ignore[misc]
127
+ def case_strategy_factory(
128
+ draw: st.DrawFn, strategies: dict[DataGenerationMethod, st.SearchStrategy] = strategies
129
+ ) -> Case:
130
+ if draw(st.integers(min_value=0, max_value=99)) < NEGATIVE_TEST_CASES_THRESHOLD:
131
+ return draw(strategies[DataGenerationMethod.negative])
132
+ return draw(strategies[DataGenerationMethod.positive])
133
+
134
+ case_strategy = case_strategy_factory()
135
+
120
136
  rules[name] = precondition(ensure_links_followed)(
121
137
  transition(
122
138
  name=name,
schemathesis/utils.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import functools
4
- import operator
5
4
  from contextlib import contextmanager
6
5
  from inspect import getfullargspec
7
6
  from pathlib import Path
@@ -14,7 +13,6 @@ from typing import (
14
13
  )
15
14
 
16
15
  import pytest
17
- from hypothesis import strategies as st
18
16
  from hypothesis.core import is_invalid_test
19
17
  from hypothesis.reporting import with_reporter
20
18
  from hypothesis.strategies import SearchStrategy
@@ -149,14 +147,6 @@ def compose(*functions: Callable) -> Callable:
149
147
  return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, noop)
150
148
 
151
149
 
152
- def combine_strategies(strategies: list[st.SearchStrategy]) -> st.SearchStrategy:
153
- """Combine a list of strategies into a single one.
154
-
155
- If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
156
- """
157
- return functools.reduce(operator.or_, strategies[1:], strategies[0])
158
-
159
-
160
150
  def skip(operation_name: str) -> NoReturn:
161
151
  raise SkipTest(f"It is not possible to generate negative test cases for `{operation_name}`")
162
152
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: schemathesis
3
- Version: 3.34.2
3
+ Version: 3.35.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
@@ -56,7 +56,26 @@ Provides-Extra: cov
56
56
  Requires-Dist: coverage-enable-subprocess; extra == 'cov'
57
57
  Requires-Dist: coverage[toml]>=5.3; extra == 'cov'
58
58
  Provides-Extra: dev
59
- Requires-Dist: schemathesis[bench,cov,docs,tests]; extra == 'dev'
59
+ Requires-Dist: aiohttp<4.0,>=3.9.1; extra == 'dev'
60
+ Requires-Dist: coverage-enable-subprocess; extra == 'dev'
61
+ Requires-Dist: coverage>=6; extra == 'dev'
62
+ Requires-Dist: coverage[toml]>=5.3; extra == 'dev'
63
+ Requires-Dist: fastapi>=0.86.0; extra == 'dev'
64
+ Requires-Dist: flask<3.0,>=2.1.1; extra == 'dev'
65
+ Requires-Dist: hypothesis-openapi<1,>=0.2; (python_version >= '3.10') and extra == 'dev'
66
+ Requires-Dist: pydantic>=1.10.2; extra == 'dev'
67
+ Requires-Dist: pytest-asyncio<1.0,>=0.18.0; extra == 'dev'
68
+ Requires-Dist: pytest-codspeed==2.2.1; extra == 'dev'
69
+ Requires-Dist: pytest-httpserver<2.0,>=1.0; extra == 'dev'
70
+ Requires-Dist: pytest-mock<4.0,>=3.7.0; extra == 'dev'
71
+ Requires-Dist: pytest-trio<1.0,>=0.8; extra == 'dev'
72
+ Requires-Dist: pytest-xdist<4.0,>=3; extra == 'dev'
73
+ Requires-Dist: sphinx; extra == 'dev'
74
+ Requires-Dist: sphinx-click; extra == 'dev'
75
+ Requires-Dist: sphinx-rtd-theme; extra == 'dev'
76
+ Requires-Dist: strawberry-graphql[fastapi]>=0.109.0; extra == 'dev'
77
+ Requires-Dist: syrupy<5.0,>=2; extra == 'dev'
78
+ Requires-Dist: trustme<1.0,>=0.9.0; extra == 'dev'
60
79
  Provides-Extra: docs
61
80
  Requires-Dist: sphinx; extra == 'docs'
62
81
  Requires-Dist: sphinx-click; extra == 'docs'
@@ -66,7 +85,7 @@ Requires-Dist: aiohttp<4.0,>=3.9.1; extra == 'tests'
66
85
  Requires-Dist: coverage>=6; extra == 'tests'
67
86
  Requires-Dist: fastapi>=0.86.0; extra == 'tests'
68
87
  Requires-Dist: flask<3.0,>=2.1.1; extra == 'tests'
69
- Requires-Dist: hypothesis-openapi<1,>=0.2; python_version >= '3.10' and extra == 'tests'
88
+ Requires-Dist: hypothesis-openapi<1,>=0.2; (python_version >= '3.10') and extra == 'tests'
70
89
  Requires-Dist: pydantic>=1.10.2; extra == 'tests'
71
90
  Requires-Dist: pytest-asyncio<1.0,>=0.18.0; extra == 'tests'
72
91
  Requires-Dist: pytest-httpserver<2.0,>=1.0; extra == 'tests'
@@ -78,10 +97,6 @@ Requires-Dist: syrupy<5.0,>=2; extra == 'tests'
78
97
  Requires-Dist: trustme<1.0,>=0.9.0; extra == 'tests'
79
98
  Description-Content-Type: text/markdown
80
99
 
81
- <p align="center">
82
- <em>Schemathesis: Supercharge your API testing, catch bugs, and ensure compliance</em>
83
- </p>
84
-
85
100
  <p align="center">
86
101
  <a href="https://github.com/schemathesis/schemathesis/actions" target="_blank">
87
102
  <img src="https://github.com/schemathesis/schemathesis/actions/workflows/build.yml/badge.svg" alt="Build">
@@ -98,22 +113,17 @@ Description-Content-Type: text/markdown
98
113
  <a href="https://discord.gg/R9ASRAmHnA" target="_blank">
99
114
  <img src="https://img.shields.io/discord/938139740912369755" alt="Discord">
100
115
  </a>
116
+ <a href="[https://discord.gg/R9ASRAmHnA](https://schemathesis.readthedocs.io/en/stable/)" target="_blank">
117
+ <img src="https://readthedocs.org/projects/schemathesis/badge/?version=stable" alt="Documentation">
118
+ </a>
101
119
  <a href="https://opensource.org/licenses/MIT" target="_blank">
102
120
  <img src="https://img.shields.io/pypi/l/schemathesis.svg" alt="License">
103
121
  </a>
104
122
  </p>
105
123
 
106
- ---
107
-
108
- **Documentation**: <a href="https://schemathesis.readthedocs.io/en/stable/" target="_blank">https://schemathesis.readthedocs.io/en/stable/ </a>
109
-
110
- **Chat**: <a href="https://discord.gg/R9ASRAmHnA" target="_blank">https://discord.gg/R9ASRAmHnA </a>
111
-
112
- ---
113
-
114
- ## What is Schemathesis?
124
+ ## Schemathesis
115
125
 
116
- 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:
126
+ 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:
117
127
 
118
128
  🎯 **Catch Hard-to-Find Bugs**
119
129
 
@@ -322,11 +332,12 @@ Schemathesis is built on top of <a href="https://hypothesis.works/" target="_bla
322
332
 
323
333
  ## Who's Using Schemathesis?
324
334
 
325
- Schemathesis is used by a number of project and companies, including direct usage or integration into other tools:
335
+ Schemathesis is used by a number of projects and companies, including direct usage or integration into other tools:
326
336
 
327
337
  - Abstract Machines ([Magistrala](https://github.com/absmach/magistrala))
328
338
  - Bundesstelle für Open Data ([smard-api](https://github.com/bundesAPI/smard-api))
329
339
  - [CheckMK](https://github.com/Checkmk/checkmk)
340
+ - [Chronosphere.io](https://github.com/chronosphereio/calyptia-api)
330
341
  - HXSecurity ([DongTai](https://github.com/HXSecurity/DongTai))
331
342
  - Netflix ([Dispatch](https://github.com/Netflix/dispatch))
332
343
  - [Pixie](https://github.com/pixie-io/pixie)
@@ -1,7 +1,7 @@
1
1
  schemathesis/__init__.py,sha256=pNaTfaC3NSdediNQuH9QAcuIx3U-MmSydvhS65FZrxw,1984
2
2
  schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
3
3
  schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
4
- schemathesis/_hypothesis.py,sha256=Igb1x3am3tafVhZ6ikY7yrmQdNthC0PZFEYVCdNAA3w,11207
4
+ schemathesis/_hypothesis.py,sha256=eLDbqhG2fBJ6DuTxXlRfadnHhFc3JE2P5zf0YS9UonQ,12782
5
5
  schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
6
6
  schemathesis/_override.py,sha256=3CbA7P9Q89W3ymaYxiOV5Xpv1yhoBqroLK4YRpYMjX4,1630
7
7
  schemathesis/_rate_limiter.py,sha256=q_XWst5hzuAyXQRiZc4s_bx7-JlPYZM_yKDmeavt3oo,242
@@ -21,34 +21,34 @@ schemathesis/models.py,sha256=WpF-W592_MVWnMZ8t7wt_A2fr3YNLEwPX3r-jwW8GpY,44266
21
21
  schemathesis/parameters.py,sha256=PndmqQRlEYsCt1kWjSShPsFf6vj7X_7FRdz_-A95eNg,2258
22
22
  schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  schemathesis/sanitization.py,sha256=_qSt04f_XcHrgguyUnowvdfj-b6u409Ubu07i0ivQUQ,9011
24
- schemathesis/schemas.py,sha256=YXTWWM1JTFNQEKrxWPye1uQgm3I1_MQPBbc2Y4KW19o,20610
24
+ schemathesis/schemas.py,sha256=nkcGcWsRCaT3wgx9vxyvluehqkWur7Fuhv8JAAu9AhY,20614
25
25
  schemathesis/serializers.py,sha256=kxXZ-UGa1v_vOm0sC4QYcrNv4rfvI7tHGT2elRVbCbc,11649
26
26
  schemathesis/targets.py,sha256=XIGRghvEzbmEJjse9aZgNEj67L3jAbiazm2rxURWgDE,2351
27
27
  schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
28
28
  schemathesis/types.py,sha256=xOzNAeMs6qqeaJnWs5Fpw5JPbvVjyfRfxTJa3G2Ln5I,920
29
- schemathesis/utils.py,sha256=NX04p9mO-lCAH3DIISXDXPxWZk6lkGNM4-ubRi8vlvY,5234
30
- schemathesis/cli/__init__.py,sha256=ZxFkRvpRAaV5OuDZeHEeWrFTnVjJXX-w4cQfYe_R6Yw,73390
29
+ schemathesis/utils.py,sha256=bYvB3l1iMxiUNHu7_1qhOj5gJf_8QUssL4Uoqgrux9A,4878
30
+ schemathesis/cli/__init__.py,sha256=O2VIpE9EWFrklWlVTTFUQGShxN8Ij9nbaOJFslDEmHY,73976
31
31
  schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
32
32
  schemathesis/cli/callbacks.py,sha256=PJs64n6qGrGC5Yv_yl3fGm797cvN6pp2enFLmSljGKs,15127
33
33
  schemathesis/cli/cassettes.py,sha256=lKKKKLjoVT8VnZXjCOCmDYhvLKn1XSrCXCMF9EeftHA,18498
34
34
  schemathesis/cli/constants.py,sha256=wk-0GsoJIel8wFFerQ6Kf_6eAYUtIWkwMFwyAqv3yj4,1635
35
- schemathesis/cli/context.py,sha256=EMeyAbU9mRujR46anc43yr6ab4rGYtIDaHC3cV9Qa-Q,2092
35
+ schemathesis/cli/context.py,sha256=6OYpSbeRobkdyDGvSj_2_zwEelM9K6fXpUCUjVC2sQM,2243
36
36
  schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
37
- schemathesis/cli/handlers.py,sha256=RjvogPCqqFTiyVYWrGG6euw9-6h-7uSeFvS-ouU8eWs,389
37
+ schemathesis/cli/handlers.py,sha256=EXSAFe5TQlHANz1AVlSttfsoDT2oeaeFbqq1N7e2udw,467
38
38
  schemathesis/cli/junitxml.py,sha256=FeQbgjYYwD0twk1QSbP3AivLPhHGwIuikQ6CN2Kw9ho,5050
39
39
  schemathesis/cli/options.py,sha256=DY5PUzpUNyYgpcqqFTeZjmVUykpbaI9fbN44QIWNOfA,2559
40
40
  schemathesis/cli/reporting.py,sha256=zNKaRAUfDZ23-gMtiWbnG2OBndkYlM7ljKvk0Mc_lqI,3589
41
41
  schemathesis/cli/sanitization.py,sha256=Onw_NWZSom6XTVNJ5NHnC0PAhrYAcGzIXJbsBCzLkn4,1005
42
42
  schemathesis/cli/output/__init__.py,sha256=AXaUzQ1nhQ-vXhW4-X-91vE2VQtEcCOrGtQXXNN55iQ,29
43
- schemathesis/cli/output/default.py,sha256=nYuUwm2M3pIsDebofgXuIFb7Vl4t4sLO0PKmJcE6tb8,39385
43
+ schemathesis/cli/output/default.py,sha256=VrV5I4PYaZPiKzbuJXmDL2PtcPUDoWxbb6pC1ShgjJ8,39508
44
44
  schemathesis/cli/output/short.py,sha256=CL6-Apxr5tuZ3BL1vecV1MiRY1wDt21g0wiUwZu6mLM,2607
45
45
  schemathesis/contrib/__init__.py,sha256=FH8NL8NXgSKBFOF8Jy_EB6T4CJEaiM-tmDhz16B2o4k,187
46
46
  schemathesis/contrib/unique_data.py,sha256=_ElPRLNp0XhdETiZ-aplKln4hgU04jxR17kmjAEWf1I,1318
47
47
  schemathesis/contrib/openapi/__init__.py,sha256=N-BoCzrLGq9aynubhmQBS-LJUBv1wPJcveMCJDhyQl4,217
48
- schemathesis/contrib/openapi/fill_missing_examples.py,sha256=vXImJkXj1UHkytsciQrmkuNu3AnTYzwrR_gODf6ObOA,584
48
+ schemathesis/contrib/openapi/fill_missing_examples.py,sha256=SL3LXG4khjGKneU3aBu1MGIhYtwRMjK77NH8L--9JBE,583
49
49
  schemathesis/contrib/openapi/formats/__init__.py,sha256=OpHWPW8MkTLVv83QXPYY1HVLflhmSH49hSVefm1oVV0,111
50
50
  schemathesis/contrib/openapi/formats/uuid.py,sha256=PG7aV0QAQnQ1zKmKiDK3cJue3Xy-TLGzyMeCB_RQbgk,391
51
- schemathesis/experimental/__init__.py,sha256=IyXUjRRbZWlj3V702bZNu-fmMgt1XHSihJHw4XrF-VI,2880
51
+ schemathesis/experimental/__init__.py,sha256=vza-jHjFAdyrnOBWVkiXyQi2Q7HoTUM9Zs3E7ozJmog,3161
52
52
  schemathesis/extra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
53
  schemathesis/extra/_aiohttp.py,sha256=-bIY0ucv7pfK3gA9PHiO4n7ajtZJJM9pS3EY3cLau9c,957
54
54
  schemathesis/extra/_flask.py,sha256=BHCbxBDoMlIvNS5sRNyrPN1NwHxiDQt0IsyfYwwoFes,289
@@ -57,7 +57,10 @@ schemathesis/extra/pytest_plugin.py,sha256=ymicV2NjmSzee0ccUUUjNEvb9ihCxxf_8M60g
57
57
  schemathesis/fixups/__init__.py,sha256=RP5QYJVJhp8LXjhH89fCRaIVU26dHCy74jD9seoYMuc,967
58
58
  schemathesis/fixups/fast_api.py,sha256=mn-KzBqnR8jl4W5fY-_ZySabMDMUnpzCIESMHnlvE1c,1304
59
59
  schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZETA,745
60
- schemathesis/generation/__init__.py,sha256=wUd5HqIh96Aw91Un0rLOd3XXi2E3BXedSMMjS5RXkuo,2472
60
+ schemathesis/generation/__init__.py,sha256=IzldWIswXBjCUaVInvXDoaXIrUbtZIU5cisnWcg2IX8,1609
61
+ schemathesis/generation/_hypothesis.py,sha256=Qel0mBsZV6tOEspRGfbJKFZevaMgHJjzY1F0Oo1bP_Y,1408
62
+ schemathesis/generation/_methods.py,sha256=jCK09f4sedDfePrS-6BIiE-CcEE8fJ4ZHxq1BHoTltQ,1101
63
+ schemathesis/generation/coverage.py,sha256=_g4jcuNTZ7emudQklcHcnkwNZeP1QBo1smNkGZHeCMo,17176
61
64
  schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
62
65
  schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
63
66
  schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
@@ -82,7 +85,7 @@ schemathesis/service/ci.py,sha256=3m9xSWFhrpWA6mI_8bXI7sspND9RSqXFM9UseO5nIiM,67
82
85
  schemathesis/service/client.py,sha256=Mpm90O59GctKVw38OcV7F99a8VzsL_Z5AT-dDkn25hc,5218
83
86
  schemathesis/service/constants.py,sha256=Q1bhtLRkmhso4KSVAtWl0u446Wlbk3ShOL3ZdbPoJOM,1502
84
87
  schemathesis/service/events.py,sha256=zgELQFB-ir8yzK1G-a5BDOyxrISuXKUQ72fIBKIdgJY,1238
85
- schemathesis/service/extensions.py,sha256=ClgL87obAMo_K4yexp2rqIuSNEpUyfhlE6OyRBMVY0U,7827
88
+ schemathesis/service/extensions.py,sha256=uU87ewgtdq610TvCRELsgFqt0YLSFb3dAHXlB0ZDQ_w,7832
86
89
  schemathesis/service/hosts.py,sha256=ad2Lxq9Zcc9PP-1eFLQnxen4ImglcGOH8n7CGG72NNg,3123
87
90
  schemathesis/service/metadata.py,sha256=x2LeCED1mdPf-YQJmjY8xtcIKHfD1ap5V0BGl-UgqNo,2087
88
91
  schemathesis/service/models.py,sha256=ihItUJ9CvH4TvmdfJY3W88NR82OODF8a3RD7WRXn6RM,6578
@@ -104,14 +107,14 @@ schemathesis/specs/openapi/checks.py,sha256=LiwoL5W_qK40j-JFSc9hfM8IGSszMBUWe71Y
104
107
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
105
108
  schemathesis/specs/openapi/converter.py,sha256=TaYgc5BBHPdkN-n0lqpbeVgLu3eL3L8Wu3y_Vo3TJaQ,2800
106
109
  schemathesis/specs/openapi/definitions.py,sha256=Z186F0gNBSCmPg-Kk7Q-n6XxEZHIOzgUyeqixlC62XE,94058
107
- schemathesis/specs/openapi/examples.py,sha256=ElfX1kZ4XWfOSOVbes_ovy4P6q_NgWpa0RF0rNJLVM4,16170
110
+ schemathesis/specs/openapi/examples.py,sha256=aE1OsR4X6OBSgRnFD0mVB2aQ3mWe65xiL2YsLb6xkeU,16169
108
111
  schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
109
112
  schemathesis/specs/openapi/links.py,sha256=DCOu14VOFqKYYFbQJHWICDpmTBzJfeP2v2FXBwW3vBI,17531
110
113
  schemathesis/specs/openapi/loaders.py,sha256=AcpvTK8qdirSRcHcinCjQbwfSQSx448LAh_GvFML1C0,25515
111
114
  schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
112
115
  schemathesis/specs/openapi/parameters.py,sha256=_6vNCnPXcdxjfAQbykCRLHjvmTpu_02xDJghxDrGYr8,13611
113
116
  schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
114
- schemathesis/specs/openapi/schemas.py,sha256=c732S1iDo-Ns-mw8xfRn98KXzhE6bgmgYk9KN9lTqhc,53003
117
+ schemathesis/specs/openapi/schemas.py,sha256=3pZEAU9sB9PT32z3BhJPsSSR0wVpJXL20-z9hMWS52s,53053
115
118
  schemathesis/specs/openapi/security.py,sha256=nEhDB_SvEFldmfpa9uOQywfWN6DtXHKmgtwucJvfN5Q,7096
116
119
  schemathesis/specs/openapi/serialization.py,sha256=5qGdFHZ3n80UlbSXrO_bkr4Al_7ci_Z3aSUjZczNDQY,11384
117
120
  schemathesis/specs/openapi/utils.py,sha256=-TCu0hTrlwp2x5qHNp-TxiHRMeIZC9OBmlhLssjRIiQ,742
@@ -127,7 +130,7 @@ schemathesis/specs/openapi/negative/__init__.py,sha256=gw0w_9tVQf_MY5Df3_xTZFC4r
127
130
  schemathesis/specs/openapi/negative/mutations.py,sha256=lLEN0GLxvPmZBQ3tHCznDSjmZ4yQiQxspjv1UpO4Kx0,19019
128
131
  schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
129
132
  schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
130
- schemathesis/specs/openapi/stateful/__init__.py,sha256=WV0iPWE_BiSoTu2bt0IMbuk71TDJvLPv403thTSXcqc,8662
133
+ schemathesis/specs/openapi/stateful/__init__.py,sha256=N-Rg9kPuDFAYu8quZbiRe8P1Qd3jz3wjMsIN19nSrhk,9449
131
134
  schemathesis/specs/openapi/stateful/statistic.py,sha256=EJK4NqeAYRYl1FtU9YEuTLyhGhPmks0bLoxUPuQlOvM,7443
132
135
  schemathesis/specs/openapi/stateful/types.py,sha256=UuGcCTFvaHsqeLN9ZeUNcbjsEwmthoT3UcHfDHchOYo,419
133
136
  schemathesis/stateful/__init__.py,sha256=gONzl3pgZ8DihjK52Wd3Ye1LeP6gnulmevHI_jEqHWI,5088
@@ -144,8 +147,8 @@ schemathesis/transports/auth.py,sha256=yELjkEkfx4g74hNrd0Db9aFf0xDJDRIwhg2vzKOTZ
144
147
  schemathesis/transports/content_types.py,sha256=VrcRQvF5T_TUjrCyrZcYF2LOwKfs3IrLcMtkVSp1ImI,2189
145
148
  schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
146
149
  schemathesis/transports/responses.py,sha256=6-gvVcRK0Ho_lSydUysBNFWoJwZEiEgf6Iv-GWkQGd8,1675
147
- schemathesis-3.34.2.dist-info/METADATA,sha256=_1OPHWfBbVS9tUFtPcJJzu5L278bmaNBTfmgpafdmN0,18373
148
- schemathesis-3.34.2.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
149
- schemathesis-3.34.2.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
150
- schemathesis-3.34.2.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
151
- schemathesis-3.34.2.dist-info/RECORD,,
150
+ schemathesis-3.35.0.dist-info/METADATA,sha256=hljiaQNfbUOdt0Dt5mIuOUzyd_Qn4gkFJDWhW2gIev8,19287
151
+ schemathesis-3.35.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
152
+ schemathesis-3.35.0.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
153
+ schemathesis-3.35.0.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
154
+ schemathesis-3.35.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.12.2
2
+ Generator: hatchling 1.25.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any