schemathesis 4.0.0a7__py3-none-any.whl → 4.0.0a9__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
@@ -8,6 +8,8 @@ from schemathesis.core.version import SCHEMATHESIS_VERSION
8
8
  from schemathesis.generation import GenerationConfig, GenerationMode, HeaderConfig
9
9
  from schemathesis.generation.case import Case
10
10
  from schemathesis.generation.targets import TargetContext, TargetFunction, target
11
+ from schemathesis.hooks import HookContext
12
+ from schemathesis.schemas import BaseSchema
11
13
 
12
14
  __version__ = SCHEMATHESIS_VERSION
13
15
 
@@ -26,6 +28,8 @@ __all__ = [
26
28
  "Response",
27
29
  "TargetContext",
28
30
  "TargetFunction",
31
+ "HookContext",
32
+ "BaseSchema",
29
33
  "__version__",
30
34
  "auth",
31
35
  "check",
@@ -251,7 +251,7 @@ class FailureGroup(BaseExceptionGroup):
251
251
  return super().__new__(cls, message, list(failures))
252
252
 
253
253
 
254
- class MessageBlock(Enum):
254
+ class MessageBlock(str, Enum):
255
255
  CASE_ID = "case_id"
256
256
  FAILURE = "failure"
257
257
  STATUS = "status"
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
10
10
  from schemathesis.engine.events import EventGenerator
11
11
 
12
12
 
13
- class PhaseName(enum.Enum):
13
+ class PhaseName(str, enum.Enum):
14
14
  """Available execution phases."""
15
15
 
16
16
  PROBING = "API probing"
@@ -51,43 +51,48 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
51
51
  status = None
52
52
  is_executed = False
53
53
 
54
- with WorkerPool(
55
- workers_num=workers_num,
56
- producer=producer,
57
- worker_factory=worker_task,
58
- ctx=engine,
59
- mode=mode,
60
- phase=phase.name,
61
- suite_id=suite_started.id,
62
- ) as pool:
63
- try:
64
- while True:
65
- try:
66
- event = pool.events_queue.get(timeout=WORKER_TIMEOUT)
67
- is_executed = True
68
- if engine.is_interrupted:
69
- raise KeyboardInterrupt
70
- yield event
71
- if isinstance(event, events.NonFatalError):
72
- status = Status.ERROR
73
- if isinstance(event, events.ScenarioFinished):
74
- if event.status != Status.SKIP and (status is None or status < event.status):
75
- status = event.status
76
- if event.status in (Status.ERROR, Status.FAILURE):
77
- engine.control.count_failure()
78
- if isinstance(event, events.Interrupted) or engine.is_interrupted:
79
- status = Status.INTERRUPTED
80
- engine.stop()
81
- if engine.has_to_stop:
82
- break # type: ignore[unreachable]
83
- except queue.Empty:
84
- if all(not worker.is_alive() for worker in pool.workers):
85
- break
86
- continue
87
- except KeyboardInterrupt:
88
- engine.stop()
89
- status = Status.INTERRUPTED
90
- yield events.Interrupted(phase=phase.name)
54
+ try:
55
+ with WorkerPool(
56
+ workers_num=workers_num,
57
+ producer=producer,
58
+ worker_factory=worker_task,
59
+ ctx=engine,
60
+ mode=mode,
61
+ phase=phase.name,
62
+ suite_id=suite_started.id,
63
+ ) as pool:
64
+ try:
65
+ while True:
66
+ try:
67
+ event = pool.events_queue.get(timeout=WORKER_TIMEOUT)
68
+ is_executed = True
69
+ if engine.is_interrupted:
70
+ raise KeyboardInterrupt
71
+ yield event
72
+ if isinstance(event, events.NonFatalError):
73
+ status = Status.ERROR
74
+ if isinstance(event, events.ScenarioFinished):
75
+ if event.status != Status.SKIP and (status is None or status < event.status):
76
+ status = event.status
77
+ if event.status in (Status.ERROR, Status.FAILURE):
78
+ engine.control.count_failure()
79
+ if isinstance(event, events.Interrupted) or engine.is_interrupted:
80
+ status = Status.INTERRUPTED
81
+ engine.stop()
82
+ if engine.has_to_stop:
83
+ break # type: ignore[unreachable]
84
+ except queue.Empty:
85
+ if all(not worker.is_alive() for worker in pool.workers):
86
+ break
87
+ continue
88
+ except KeyboardInterrupt:
89
+ # Soft stop, waiting for workers to terminate
90
+ engine.stop()
91
+ status = Status.INTERRUPTED
92
+ yield events.Interrupted(phase=phase.name)
93
+ except KeyboardInterrupt:
94
+ # Hard stop, don't wait for worker threads
95
+ pass
91
96
 
92
97
  if not is_executed:
93
98
  phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
@@ -37,6 +37,8 @@ def json_recursive_strategy(strategy: st.SearchStrategy) -> st.SearchStrategy:
37
37
 
38
38
 
39
39
  BUFFER_SIZE = 8 * 1024
40
+ NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN = 100
41
+ NEGATIVE_MODE_MAX_ITEMS = 15
40
42
  FLOAT_STRATEGY: st.SearchStrategy = st.floats(allow_nan=False, allow_infinity=False).map(_replace_zero_with_nonzero)
41
43
  NUMERIC_STRATEGY: st.SearchStrategy = st.integers() | FLOAT_STRATEGY
42
44
  JSON_STRATEGY: st.SearchStrategy = st.recursive(
@@ -150,7 +152,7 @@ class CoverageContext:
150
152
 
151
153
  def is_valid_for_location(self, value: Any) -> bool:
152
154
  if self.location in ("header", "cookie") and isinstance(value, str):
153
- return is_latin_1_encodable(value) and not has_invalid_characters("", value)
155
+ return not value or (is_latin_1_encodable(value) and not has_invalid_characters("", value))
154
156
  return True
155
157
 
156
158
  def generate_from(self, strategy: st.SearchStrategy) -> Any:
@@ -257,6 +259,9 @@ def _cover_positive_for_type(
257
259
  if ty == "object" or ty == "array":
258
260
  template_schema = _get_template_schema(schema, ty)
259
261
  template = ctx.generate_from_schema(template_schema)
262
+ elif "properties" in schema or "required" in schema:
263
+ template_schema = _get_template_schema(schema, "object")
264
+ template = ctx.generate_from_schema(template_schema)
260
265
  else:
261
266
  template = None
262
267
  if GenerationMode.POSITIVE in ctx.generation_modes:
@@ -295,6 +300,8 @@ def _cover_positive_for_type(
295
300
  yield from _positive_array(ctx, schema, cast(list, template))
296
301
  elif ty == "object":
297
302
  yield from _positive_object(ctx, schema, cast(dict, template))
303
+ elif "properties" in schema or "required" in schema:
304
+ yield from _positive_object(ctx, schema, cast(dict, template))
298
305
 
299
306
 
300
307
  @contextmanager
@@ -387,41 +394,58 @@ def cover_schema_iter(
387
394
  yield value_
388
395
  seen.add(k)
389
396
  elif key == "minLength" and 0 < value < BUFFER_SIZE:
390
- with suppress(InvalidArgument):
391
- min_length = max_length = value - 1
392
- new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
393
- new_schema.setdefault("type", "string")
394
- if "pattern" in new_schema:
395
- new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
396
- if new_schema["pattern"] == schema["pattern"]:
397
- # Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
398
- del new_schema["minLength"]
399
- del new_schema["maxLength"]
400
- value = ctx.generate_from_schema(new_schema)[:max_length]
401
- else:
402
- value = ctx.generate_from_schema(new_schema)
403
- else:
404
- value = ctx.generate_from_schema(new_schema)
397
+ if value == 1:
398
+ # In this case, the only possible negative string is an empty one
399
+ # The `pattern` value may require an non-empty one and the generation will fail
400
+ # However, it is fine to violate `pattern` here as it is negative string generation anyway
401
+ value = ""
405
402
  k = _to_hashable_key(value)
406
403
  if k not in seen:
407
404
  yield NegativeValue(
408
405
  value, description="String smaller than minLength", location=ctx.current_path
409
406
  )
410
407
  seen.add(k)
408
+ else:
409
+ with suppress(InvalidArgument):
410
+ min_length = max_length = value - 1
411
+ new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
412
+ new_schema.setdefault("type", "string")
413
+ if "pattern" in new_schema:
414
+ new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
415
+ if new_schema["pattern"] == schema["pattern"]:
416
+ # Pattern wasn't updated, try to generate a valid value then shrink the string to the required length
417
+ del new_schema["minLength"]
418
+ del new_schema["maxLength"]
419
+ value = ctx.generate_from_schema(new_schema)[:max_length]
420
+ else:
421
+ value = ctx.generate_from_schema(new_schema)
422
+ else:
423
+ value = ctx.generate_from_schema(new_schema)
424
+ k = _to_hashable_key(value)
425
+ if k not in seen:
426
+ yield NegativeValue(
427
+ value, description="String smaller than minLength", location=ctx.current_path
428
+ )
429
+ seen.add(k)
411
430
  elif key == "maxLength" and value < BUFFER_SIZE:
412
431
  try:
413
432
  min_length = max_length = value + 1
414
433
  new_schema = {**schema, "minLength": min_length, "maxLength": max_length}
415
434
  new_schema.setdefault("type", "string")
416
435
  if "pattern" in new_schema:
417
- new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
418
- if new_schema["pattern"] == schema["pattern"]:
419
- # Pattern wasn't updated, try to generate a valid value then extend the string to the required length
420
- del new_schema["minLength"]
421
- del new_schema["maxLength"]
422
- value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
423
- else:
436
+ if value > NEGATIVE_MODE_MAX_LENGTH_WITH_PATTERN:
437
+ # Large `maxLength` value can be extremely slow to generate when combined with `pattern`
438
+ del new_schema["pattern"]
424
439
  value = ctx.generate_from_schema(new_schema)
440
+ else:
441
+ new_schema["pattern"] = update_quantifier(schema["pattern"], min_length, max_length)
442
+ if new_schema["pattern"] == schema["pattern"]:
443
+ # Pattern wasn't updated, try to generate a valid value then extend the string to the required length
444
+ del new_schema["minLength"]
445
+ del new_schema["maxLength"]
446
+ value = ctx.generate_from_schema(new_schema).ljust(max_length, "0")
447
+ else:
448
+ value = ctx.generate_from_schema(new_schema)
425
449
  else:
426
450
  value = ctx.generate_from_schema(new_schema)
427
451
  k = _to_hashable_key(value)
@@ -437,6 +461,61 @@ def cover_schema_iter(
437
461
  elif key == "required":
438
462
  template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
439
463
  yield from _negative_required(ctx, template, value)
464
+ elif key == "maxItems" and isinstance(value, int) and value < BUFFER_SIZE:
465
+ if value > NEGATIVE_MODE_MAX_ITEMS:
466
+ # It could be extremely slow to generate large arrays
467
+ # Generate values up to the limit and reuse them to construct the final array
468
+ new_schema = {
469
+ **schema,
470
+ "minItems": NEGATIVE_MODE_MAX_ITEMS,
471
+ "maxItems": NEGATIVE_MODE_MAX_ITEMS,
472
+ "type": "array",
473
+ }
474
+ array_value = ctx.generate_from_schema(new_schema)
475
+ # Extend the array to be of length value + 1 by repeating its own elements
476
+ diff = value + 1 - len(array_value)
477
+ if diff > 0:
478
+ array_value += (
479
+ array_value * (diff // len(array_value)) + array_value[: diff % len(array_value)]
480
+ )
481
+ k = _to_hashable_key(array_value)
482
+ if k not in seen:
483
+ yield NegativeValue(
484
+ array_value,
485
+ description="Array with more items than allowed by maxItems",
486
+ location=ctx.current_path,
487
+ )
488
+ seen.add(k)
489
+ else:
490
+ try:
491
+ # Force the array to have one more item than allowed
492
+ new_schema = {**schema, "minItems": value + 1, "maxItems": value + 1, "type": "array"}
493
+ array_value = ctx.generate_from_schema(new_schema)
494
+ k = _to_hashable_key(array_value)
495
+ if k not in seen:
496
+ yield NegativeValue(
497
+ array_value,
498
+ description="Array with more items than allowed by maxItems",
499
+ location=ctx.current_path,
500
+ )
501
+ seen.add(k)
502
+ except (InvalidArgument, Unsatisfiable):
503
+ pass
504
+ elif key == "minItems" and isinstance(value, int) and value > 0:
505
+ try:
506
+ # Force the array to have one less item than the minimum
507
+ new_schema = {**schema, "minItems": value - 1, "maxItems": value - 1, "type": "array"}
508
+ array_value = ctx.generate_from_schema(new_schema)
509
+ k = _to_hashable_key(array_value)
510
+ if k not in seen:
511
+ yield NegativeValue(
512
+ array_value,
513
+ description="Array with fewer items than allowed by minItems",
514
+ location=ctx.current_path,
515
+ )
516
+ seen.add(k)
517
+ except (InvalidArgument, Unsatisfiable):
518
+ pass
440
519
  elif (
441
520
  key == "additionalProperties"
442
521
  and not value
@@ -653,19 +732,22 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
653
732
 
654
733
  if example or examples or default:
655
734
  if example:
735
+ seen.add(_to_hashable_key(example))
656
736
  yield PositiveValue(example, description="Example value")
657
737
  if examples:
658
738
  for example in examples:
739
+ seen.add(_to_hashable_key(example))
659
740
  yield PositiveValue(example, description="Example value")
660
741
  if (
661
742
  default
662
743
  and not (example is not None and default == example)
663
744
  and not (examples is not None and any(default == ex for ex in examples))
664
745
  ):
746
+ seen.add(_to_hashable_key(default))
665
747
  yield PositiveValue(default, description="Default value")
666
748
  else:
667
749
  yield PositiveValue(template, description="Valid array")
668
- seen.add(len(template))
750
+ seen.add(_to_hashable_key(template))
669
751
 
670
752
  # Boundary and near-boundary sizes
671
753
  min_items = schema.get("minItems")
@@ -676,19 +758,19 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
676
758
  # One item more than minimum if possible
677
759
  larger = min_items + 1
678
760
  if larger not in seen and (max_items is None or larger <= max_items):
679
- yield PositiveValue(
680
- ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger}),
681
- description="Near-boundary items array",
682
- )
683
- seen.add(larger)
761
+ value = ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger})
762
+ key = _to_hashable_key(value)
763
+ if key not in seen:
764
+ seen.add(key)
765
+ yield PositiveValue(value, description="Near-boundary items array")
684
766
 
685
767
  if max_items is not None:
686
768
  if max_items < BUFFER_SIZE and max_items not in seen:
687
- yield PositiveValue(
688
- ctx.generate_from_schema({**schema, "minItems": max_items}),
689
- description="Maximum items array",
690
- )
691
- seen.add(max_items)
769
+ value = ctx.generate_from_schema({**schema, "minItems": max_items})
770
+ key = _to_hashable_key(value)
771
+ if key not in seen:
772
+ seen.add(key)
773
+ yield PositiveValue(value, description="Maximum items array")
692
774
 
693
775
  # One item smaller than maximum if possible
694
776
  smaller = max_items - 1
@@ -698,11 +780,26 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
698
780
  and smaller not in seen
699
781
  and (min_items is None or smaller >= min_items)
700
782
  ):
701
- yield PositiveValue(
702
- ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller}),
703
- description="Near-boundary items array",
704
- )
705
- seen.add(smaller)
783
+ value = ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller})
784
+ key = _to_hashable_key(value)
785
+ if key not in seen:
786
+ seen.add(key)
787
+ yield PositiveValue(value, description="Near-boundary items array")
788
+
789
+ if "items" in schema and "enum" in schema["items"] and isinstance(schema["items"]["enum"], list) and max_items != 0:
790
+ # Ensure there is enough items to pass `minItems` if it is specified
791
+ length = min_items or 1
792
+ for variant in schema["items"]["enum"]:
793
+ value = [variant] * length
794
+ key = _to_hashable_key(value)
795
+ if key not in seen:
796
+ seen.add(key)
797
+ yield PositiveValue(value, description="Enum value from available for items array")
798
+ elif min_items is None and max_items is None and "items" in schema and isinstance(schema["items"], dict):
799
+ # Otherwise only an empty array is generated
800
+ sub_schema = schema["items"]
801
+ for item in cover_schema_iter(ctx, sub_schema):
802
+ yield PositiveValue([item.value], description=f"Single-item array: {item.description}")
706
803
 
707
804
 
708
805
  def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
@@ -770,7 +867,12 @@ def _negative_enum(
770
867
  _hashed = _to_hashable_key(x)
771
868
  return _hashed not in seen
772
869
 
773
- strategy = (st.none() | st.booleans() | NUMERIC_STRATEGY | st.text()).filter(is_not_in_value)
870
+ strategy = (
871
+ st.text(alphabet=st.characters(min_codepoint=65, max_codepoint=122, categories=["L"]), min_size=3)
872
+ | st.none()
873
+ | st.booleans()
874
+ | NUMERIC_STRATEGY
875
+ ).filter(is_not_in_value)
774
876
  value = ctx.generate_from(strategy)
775
877
  yield NegativeValue(value, description="Invalid enum value", location=ctx.current_path)
776
878
  hashed = _to_hashable_key(value)
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
4
+ import os
3
5
  from dataclasses import dataclass, field
4
6
  from enum import Enum
5
7
  from functools import wraps
@@ -10,12 +12,13 @@ from typing import Any, Callable, Generator, Mapping
10
12
  import hypothesis
11
13
  from hypothesis import Phase
12
14
  from hypothesis import strategies as st
15
+ from hypothesis._settings import all_settings
13
16
  from hypothesis.errors import Unsatisfiable
14
17
  from jsonschema.exceptions import SchemaError
15
18
 
16
19
  from schemathesis import auths
17
20
  from schemathesis.auths import AuthStorage, AuthStorageMark
18
- from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
21
+ from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types, string_to_boolean
19
22
  from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
20
23
  from schemathesis.core.marks import Mark
21
24
  from schemathesis.core.transport import prepare_urlencoded
@@ -38,7 +41,7 @@ from schemathesis.schemas import APIOperation, ParameterSet
38
41
  setup()
39
42
 
40
43
 
41
- class HypothesisTestMode(Enum):
44
+ class HypothesisTestMode(str, Enum):
42
45
  EXAMPLES = "examples"
43
46
  COVERAGE = "coverage"
44
47
  FUZZING = "fuzzing"
@@ -96,7 +99,11 @@ def create_test(
96
99
  # Merge the user-provided settings with the current ones
97
100
  settings = hypothesis.settings(
98
101
  settings,
99
- **{item: value for item, value in config.settings.__dict__.items() if value != getattr(default, item)},
102
+ **{
103
+ item: getattr(config.settings, item)
104
+ for item in all_settings
105
+ if getattr(config.settings, item) != getattr(default, item)
106
+ },
100
107
  )
101
108
 
102
109
  if Phase.explain in settings.phases:
@@ -120,8 +127,11 @@ def create_test(
120
127
  ):
121
128
  hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
122
129
 
130
+ disable_coverage = string_to_boolean(os.getenv("SCHEMATHESIS_DISABLE_COVERAGE", ""))
131
+
123
132
  if (
124
- HypothesisTestMode.COVERAGE in config.modes
133
+ not disable_coverage
134
+ and HypothesisTestMode.COVERAGE in config.modes
125
135
  and Phase.explicit in settings.phases
126
136
  and specification.supports_feature(SpecificationFeature.COVERAGE)
127
137
  and not config.given_args
@@ -158,7 +168,24 @@ def create_base_test(
158
168
  __tracebackhide__ = True
159
169
  return test_function(*args, **kwargs)
160
170
 
161
- return hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
171
+ funcobj = hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
172
+
173
+ if asyncio.iscoroutinefunction(test_function):
174
+ funcobj.hypothesis.inner_test = make_async_test(test_function) # type: ignore
175
+ return funcobj
176
+
177
+
178
+ def make_async_test(test: Callable) -> Callable:
179
+ def async_run(*args: Any, **kwargs: Any) -> None:
180
+ try:
181
+ loop = asyncio.get_event_loop()
182
+ except RuntimeError:
183
+ loop = asyncio.new_event_loop()
184
+ coro = test(*args, **kwargs)
185
+ future = asyncio.ensure_future(coro, loop=loop)
186
+ loop.run_until_complete(future)
187
+
188
+ return async_run
162
189
 
163
190
 
164
191
  def add_examples(
schemathesis/hooks.py CHANGED
@@ -21,7 +21,7 @@ HookDispatcherMark = Mark["HookDispatcher"](attr_name="hook_dispatcher")
21
21
 
22
22
 
23
23
  @unique
24
- class HookScope(Enum):
24
+ class HookScope(int, Enum):
25
25
  GLOBAL = 1
26
26
  SCHEMA = 2
27
27
  TEST = 3
@@ -108,7 +108,12 @@ class SchemathesisCase(PyCollector):
108
108
  This implementation is based on the original one in pytest, but with slight adjustments
109
109
  to produce tests out of hypothesis ones.
110
110
  """
111
- from schemathesis.generation.hypothesis.builder import HypothesisTestConfig, HypothesisTestMode, create_test
111
+ from schemathesis.generation.hypothesis.builder import (
112
+ HypothesisTestConfig,
113
+ HypothesisTestMode,
114
+ create_test,
115
+ make_async_test,
116
+ )
112
117
 
113
118
  is_trio_test = False
114
119
  for mark in getattr(self.test_function, "pytestmark", []):
@@ -221,19 +226,6 @@ class SchemathesisCase(PyCollector):
221
226
  pytest.fail("Error during collection")
222
227
 
223
228
 
224
- def make_async_test(test: Callable) -> Callable:
225
- def async_run(*args: Any, **kwargs: Any) -> None:
226
- try:
227
- loop = asyncio.get_event_loop()
228
- except RuntimeError:
229
- loop = asyncio.new_event_loop()
230
- coro = test(*args, **kwargs)
231
- future = asyncio.ensure_future(coro, loop=loop)
232
- loop.run_until_complete(future)
233
-
234
- return async_run
235
-
236
-
237
229
  @hookimpl(hookwrapper=True) # type:ignore
238
230
  def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -> Generator[None, Any, None]:
239
231
  """Switch to a different collector if the test is parametrized marked by schemathesis."""
schemathesis/schemas.py CHANGED
@@ -14,7 +14,7 @@ from typing import (
14
14
  NoReturn,
15
15
  TypeVar,
16
16
  )
17
- from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
17
+ from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
18
18
 
19
19
  from schemathesis import transport
20
20
  from schemathesis.core import NOT_SET, NotSet
@@ -436,6 +436,8 @@ class BaseSchema(Mapping):
436
436
  app: Any | NotSet = NOT_SET,
437
437
  ) -> Self:
438
438
  if not isinstance(base_url, NotSet):
439
+ if base_url is not None:
440
+ validate_base_url(base_url)
439
441
  self.base_url = base_url
440
442
  if not isinstance(location, NotSet):
441
443
  self.location = location
@@ -453,6 +455,21 @@ class BaseSchema(Mapping):
453
455
  return self
454
456
 
455
457
 
458
+ INVALID_BASE_URL_MESSAGE = (
459
+ "The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
460
+ "Make sure it is a properly formatted URL."
461
+ )
462
+
463
+
464
+ def validate_base_url(value: str) -> None:
465
+ try:
466
+ netloc = urlparse(value).netloc
467
+ except ValueError as exc:
468
+ raise ValueError(INVALID_BASE_URL_MESSAGE) from exc
469
+ if value and not netloc:
470
+ raise ValueError(INVALID_BASE_URL_MESSAGE)
471
+
472
+
456
473
  @dataclass
457
474
  class APIOperationMap(Mapping):
458
475
  _schema: BaseSchema
@@ -460,7 +460,7 @@ def ensure_resource_availability(ctx: CheckContext, response: Response, case: Ca
460
460
  )
461
461
 
462
462
 
463
- class AuthKind(enum.Enum):
463
+ class AuthKind(str, enum.Enum):
464
464
  EXPLICIT = "explicit"
465
465
  GENERATED = "generated"
466
466
 
@@ -6,7 +6,7 @@ from typing import Callable, Generator
6
6
 
7
7
 
8
8
  @unique
9
- class TokenType(Enum):
9
+ class TokenType(int, Enum):
10
10
  VARIABLE = 1
11
11
  STRING = 2
12
12
  POINTER = 3
@@ -25,7 +25,7 @@ class Node:
25
25
 
26
26
 
27
27
  @unique
28
- class NodeType(Enum):
28
+ class NodeType(str, Enum):
29
29
  URL = "$url"
30
30
  METHOD = "$method"
31
31
  STATUS_CODE = "$statusCode"
@@ -20,7 +20,7 @@ from .utils import can_negate
20
20
  T = TypeVar("T")
21
21
 
22
22
 
23
- class MutationResult(enum.Enum):
23
+ class MutationResult(int, enum.Enum):
24
24
  """The result of applying some mutation to some schema.
25
25
 
26
26
  Failing to mutate something means that by applying some mutation, it is not possible to change
@@ -134,6 +134,7 @@ class OpenAPI20Parameter(OpenAPIParameter):
134
134
  "multipleOf",
135
135
  "example",
136
136
  "examples",
137
+ "default",
137
138
  )
138
139
 
139
140
 
@@ -178,6 +179,7 @@ class OpenAPI30Parameter(OpenAPIParameter):
178
179
  "format",
179
180
  "example",
180
181
  "examples",
182
+ "default",
181
183
  )
182
184
 
183
185
  def from_open_api_to_json_schema(self, operation: APIOperation, open_api_schema: dict[str, Any]) -> dict[str, Any]:
@@ -230,6 +232,7 @@ class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
230
232
  "additionalProperties",
231
233
  "example",
232
234
  "examples",
235
+ "default",
233
236
  )
234
237
  # NOTE. For Open API 2.0 bodies, we still give `x-example` precedence over the schema-level `example` field to keep
235
238
  # the precedence rules consistent.
@@ -69,13 +69,18 @@ def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, m
69
69
  trailing_anchor_length = _get_anchor_length(parsed[2][1])
70
70
  leading_anchor = pattern[:leading_anchor_length]
71
71
  trailing_anchor = pattern[-trailing_anchor_length:]
72
- return (
73
- leading_anchor
74
- + _update_quantifier(
75
- op, value, pattern[leading_anchor_length:-trailing_anchor_length], min_length, max_length
76
- )
77
- + trailing_anchor
78
- )
72
+ # Special case for patterns canonicalisation. Some frameworks generate `\\w\\W` instead of `.`
73
+ # Such patterns lead to significantly slower data generation
74
+ if op == sre.IN and _matches_anything(value):
75
+ op = sre.ANY
76
+ value = None
77
+ inner_pattern = "."
78
+ elif op in REPEATS and len(value[2]) == 1 and value[2][0][0] == sre.IN and _matches_anything(value[2][0][1]):
79
+ value = (value[0], value[1], [(sre.ANY, None)], *value[3:])
80
+ inner_pattern = "."
81
+ else:
82
+ inner_pattern = pattern[leading_anchor_length:-trailing_anchor_length]
83
+ return leading_anchor + _update_quantifier(op, value, inner_pattern, min_length, max_length) + trailing_anchor
79
84
  elif (
80
85
  len(parsed) > 3
81
86
  and parsed[0][0] == ANCHOR
@@ -86,6 +91,19 @@ def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, m
86
91
  return pattern
87
92
 
88
93
 
94
+ def _matches_anything(value: list) -> bool:
95
+ """Check if the given pattern is equivalent to '.' (match any character)."""
96
+ # Common forms: [\w\W], [\s\S], etc.
97
+ return value in (
98
+ [(sre.CATEGORY, sre.CATEGORY_WORD), (sre.CATEGORY, sre.CATEGORY_NOT_WORD)],
99
+ [(sre.CATEGORY, sre.CATEGORY_SPACE), (sre.CATEGORY, sre.CATEGORY_NOT_SPACE)],
100
+ [(sre.CATEGORY, sre.CATEGORY_DIGIT), (sre.CATEGORY, sre.CATEGORY_NOT_DIGIT)],
101
+ [(sre.CATEGORY, sre.CATEGORY_NOT_WORD), (sre.CATEGORY, sre.CATEGORY_WORD)],
102
+ [(sre.CATEGORY, sre.CATEGORY_NOT_SPACE), (sre.CATEGORY, sre.CATEGORY_SPACE)],
103
+ [(sre.CATEGORY, sre.CATEGORY_NOT_DIGIT), (sre.CATEGORY, sre.CATEGORY_DIGIT)],
104
+ )
105
+
106
+
89
107
  def _handle_anchored_pattern(parsed: list, pattern: str, min_length: int | None, max_length: int | None) -> str:
90
108
  """Update regex pattern with multiple quantified patterns to satisfy length constraints."""
91
109
  # Extract anchors
@@ -269,15 +287,28 @@ def _get_anchor_length(node_type: int) -> int:
269
287
  return 1 # ^ or $ or their multiline/locale/unicode variants
270
288
 
271
289
 
272
- def _update_quantifier(op: int, value: tuple, pattern: str, min_length: int | None, max_length: int | None) -> str:
290
+ def _update_quantifier(
291
+ op: int, value: tuple | None, pattern: str, min_length: int | None, max_length: int | None
292
+ ) -> str:
273
293
  """Update the quantifier based on the operation type and given constraints."""
274
- if op in REPEATS:
294
+ if op in REPEATS and value is not None:
275
295
  return _handle_repeat_quantifier(value, pattern, min_length, max_length)
276
296
  if op in (LITERAL, IN) and max_length != 0:
277
297
  return _handle_literal_or_in_quantifier(pattern, min_length, max_length)
298
+ if op == sre.ANY and value is None:
299
+ # Equivalent to `.` which is in turn is the same as `.{1}`
300
+ return _handle_repeat_quantifier(
301
+ SINGLE_ANY,
302
+ pattern,
303
+ min_length,
304
+ max_length,
305
+ )
278
306
  return pattern
279
307
 
280
308
 
309
+ SINGLE_ANY = sre_parse.parse(".{1}")[0][1]
310
+
311
+
281
312
  def _handle_repeat_quantifier(
282
313
  value: tuple[int, int, tuple], pattern: str, min_length: int | None, max_length: int | None
283
314
  ) -> str:
@@ -139,7 +139,10 @@ class ConvertingResolver(InliningResolver):
139
139
  def resolve(self, ref: str) -> tuple[str, Any]:
140
140
  url, document = super().resolve(ref)
141
141
  document = to_json_schema_recursive(
142
- document, nullable_name=self.nullable_name, is_response_schema=self.is_response_schema
142
+ document,
143
+ nullable_name=self.nullable_name,
144
+ is_response_schema=self.is_response_schema,
145
+ update_quantifiers=False,
143
146
  )
144
147
  return url, document
145
148
 
@@ -1022,24 +1022,30 @@ class SwaggerV20(BaseOpenAPISchema):
1022
1022
  content_types = self.get_request_payload_content_types(operation)
1023
1023
  is_multipart = "multipart/form-data" in content_types
1024
1024
 
1025
- def add_file(file_value: Any) -> None:
1026
- if isinstance(file_value, list):
1027
- for item in file_value:
1028
- files.append((name, (None, item)))
1029
- else:
1030
- files.append((name, file_value))
1025
+ known_fields: dict[str, dict] = {}
1031
1026
 
1032
1027
  for parameter in operation.body:
1033
1028
  if isinstance(parameter, OpenAPI20CompositeBody):
1034
1029
  for form_parameter in parameter.definition:
1035
- name = form_parameter.name
1036
- # It might be not in `form_data`, if the parameter is optional
1037
- if name in form_data:
1038
- value = form_data[name]
1039
- if form_parameter.definition.get("type") == "file" or is_multipart:
1040
- add_file(value)
1041
- else:
1042
- data[name] = value
1030
+ known_fields[form_parameter.name] = form_parameter.definition
1031
+
1032
+ def add_file(name: str, value: Any) -> None:
1033
+ if isinstance(value, list):
1034
+ for item in value:
1035
+ files.append((name, (None, item)))
1036
+ else:
1037
+ files.append((name, value))
1038
+
1039
+ for name, value in form_data.items():
1040
+ param_def = known_fields.get(name)
1041
+ if param_def:
1042
+ if param_def.get("type") == "file" or is_multipart:
1043
+ add_file(name, value)
1044
+ else:
1045
+ data[name] = value
1046
+ else:
1047
+ # Unknown field — treat it as a file (safe default under multipart/form-data)
1048
+ add_file(name, value)
1043
1049
  # `None` is the default value for `files` and `data` arguments in `requests.request`
1044
1050
  return files or None, data or None
1045
1051
 
@@ -1189,7 +1195,11 @@ class OpenApi30(SwaggerV20):
1189
1195
  files = []
1190
1196
  definition = operation.definition.raw
1191
1197
  if "$ref" in definition["requestBody"]:
1192
- body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
1198
+ self.resolver.push_scope(operation.definition.scope)
1199
+ try:
1200
+ body = self.resolver.resolve_all(definition["requestBody"], RECURSION_DEPTH_LIMIT)
1201
+ finally:
1202
+ self.resolver.pop_scope()
1193
1203
  else:
1194
1204
  body = definition["requestBody"]
1195
1205
  content = body["content"]
@@ -1202,14 +1212,19 @@ class OpenApi30(SwaggerV20):
1202
1212
  break
1203
1213
  else:
1204
1214
  raise InternalError("No 'multipart/form-data' media type found in the schema")
1205
- for name, property_schema in (schema or {}).get("properties", {}).items():
1206
- if name in form_data:
1207
- if isinstance(form_data[name], list):
1208
- files.extend([(name, item) for item in form_data[name]])
1215
+ for name, value in form_data.items():
1216
+ property_schema = (schema or {}).get("properties", {}).get(name)
1217
+ if property_schema:
1218
+ if isinstance(value, list):
1219
+ files.extend([(name, item) for item in value])
1209
1220
  elif property_schema.get("format") in ("binary", "base64"):
1210
- files.append((name, form_data[name]))
1221
+ files.append((name, value))
1211
1222
  else:
1212
- files.append((name, (None, form_data[name])))
1223
+ files.append((name, (None, value)))
1224
+ elif isinstance(value, list):
1225
+ files.extend([(name, item) for item in value])
1226
+ else:
1227
+ files.append((name, (None, value)))
1213
1228
  # `None` is the default value for `files` and `data` arguments in `requests.request`
1214
1229
  return files or None, None
1215
1230
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 4.0.0a7
3
+ Version: 4.0.0a9
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
@@ -1,11 +1,11 @@
1
- schemathesis/__init__.py,sha256=ggp1CxctLo__wwFwlDhvtrexxDXGSbRjFKzXw_Twi7k,1139
1
+ schemathesis/__init__.py,sha256=S9MD8cGyXWihyQikye9mSBpvrfUJbOItD5yr65vkx6A,1263
2
2
  schemathesis/auths.py,sha256=t-YuPyoLqL7jlRUH-45JxO7Ir3pYxpe31CRmNIJh7rI,15423
3
3
  schemathesis/checks.py,sha256=B5-ROnjvvwpaqgj_iQ7eCjGqvRRVT30eWNPLKmwdrM8,5084
4
4
  schemathesis/errors.py,sha256=VSZ-h9Bt7QvrvywOGB-MoHCshR8OWJegYlBxfVh5Vuw,899
5
5
  schemathesis/filters.py,sha256=CzVPnNSRLNgvLlU5_WssPEC0wpdQi0dMvDpHSQbAlkE,13577
6
- schemathesis/hooks.py,sha256=jTdN5GJbxHRMshxgcuI_th9ouuL32CN4m2Jt0pmT_bs,13148
6
+ schemathesis/hooks.py,sha256=ZSGEnsLJ7UVezf4CcaJebVkjEpvwgJolJFZo5fjQNDc,13153
7
7
  schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- schemathesis/schemas.py,sha256=Hs2pJTUa2Od3x5YRprrKYIfE7ndpKbAqeDylAWkB6yM,27407
8
+ schemathesis/schemas.py,sha256=A2qAs1PY9wbRWk6PFnslWyIqzchAhu5oo_MsKL7uF8w,27952
9
9
  schemathesis/cli/__init__.py,sha256=U9gjzWWpiFhaqevPjZbwyTNjABdpvXETI4HgwdGKnvs,877
10
10
  schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
11
11
  schemathesis/cli/constants.py,sha256=rUixnqorraUFDtOu3Nmm1x_k0qbgmW9xW96kQB_fBCQ,338
@@ -40,7 +40,7 @@ schemathesis/core/control.py,sha256=IzwIc8HIAEMtZWW0Q0iXI7T1niBpjvcLlbuwOSmy5O8,
40
40
  schemathesis/core/curl.py,sha256=yuaCe_zHLGwUjEeloQi6W3tOA3cGdnHDNI17-5jia0o,1723
41
41
  schemathesis/core/deserialization.py,sha256=ygIj4fNaOd0mJ2IvTsn6bsabBt_2AbSLCz-z9UqfpdQ,2406
42
42
  schemathesis/core/errors.py,sha256=97Fk3udsMaS5xZrco7ZaShqe4W6g2aZ55J7d58HPRac,15881
43
- schemathesis/core/failures.py,sha256=jbbxOXB8LDYoLI97YrLCKi9XLuSqVqFJSLeeixVJPRU,8828
43
+ schemathesis/core/failures.py,sha256=nt_KJAQnachw4Ey-rZ__P8q6nGJ_YekZiSLc6-PfFW0,8833
44
44
  schemathesis/core/fs.py,sha256=ItQT0_cVwjDdJX9IiI7EnU75NI2H3_DCEyyUjzg_BgI,472
45
45
  schemathesis/core/lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
46
46
  schemathesis/core/loaders.py,sha256=SQQ-8m64-D2FaOgvwKZLyTtLJuzP3RPo7Ud2BERK1c0,3404
@@ -63,24 +63,24 @@ schemathesis/engine/core.py,sha256=DfulRMVTivmZj-wwLekIhuSzLsFnuVPtSg7j9HyWdz0,5
63
63
  schemathesis/engine/errors.py,sha256=8PHYsuq2qIEJHm2FDf_UnWa4IDc-DRFTPckLAr22yhE,16895
64
64
  schemathesis/engine/events.py,sha256=gslRAWQKMPqBCQzLDS4wAbsKcVuONSy5SPqimJJJYT4,6250
65
65
  schemathesis/engine/recorder.py,sha256=K3HfMARrT5mPWXPnYebjjcq5CcsBRhMrtZwEL9_Lvtg,8432
66
- schemathesis/engine/phases/__init__.py,sha256=3p_T3JYBFOtrwtgmMM7J-6a41QJgk83dUtm7NKcVl3o,2490
66
+ schemathesis/engine/phases/__init__.py,sha256=CuTBMaQIsGdtWw400maiwqfIbMyVv5_vHXV-SY5A5NI,2495
67
67
  schemathesis/engine/phases/probes.py,sha256=3M9g3E7CXbDDK_8inuvkRZibCCcoO2Ce5U3lnyTeWXQ,5131
68
68
  schemathesis/engine/phases/stateful/__init__.py,sha256=lWo2RLrutNblHvohTzofQqL22GORwBRA8bf6jvLuGPg,2391
69
69
  schemathesis/engine/phases/stateful/_executor.py,sha256=m1ZMqFUPc4Hdql10l0gF3tpP4JOImSA-XeBd4jg3Ll8,12443
70
70
  schemathesis/engine/phases/stateful/context.py,sha256=SKWsok-tlWbUDagiUmP7cLNW6DsgFDc_Afv0vQfWv6c,2964
71
- schemathesis/engine/phases/unit/__init__.py,sha256=LcBQpGNPeEFB9XPGpcHBcH-C7nF-e8bZNPop9PIfiKA,7861
71
+ schemathesis/engine/phases/unit/__init__.py,sha256=QmtzIgP9KWLo-IY1kMyBqYXPMxFQz-WF2eVTWewqUfI,8174
72
72
  schemathesis/engine/phases/unit/_executor.py,sha256=buMEr7e01SFSeNuEQNGMf4hoiLxX9_sp0JhH4LBAk9M,12928
73
73
  schemathesis/engine/phases/unit/_pool.py,sha256=9OgmFd-ov1AAvcZGquK40PXkGLp7f2qCjZoPZuoZl4A,2529
74
74
  schemathesis/experimental/__init__.py,sha256=jYY3Mq6okqTRTMudPzcaT0JVjzJW5IN_ZVJdGU0stBs,2011
75
75
  schemathesis/generation/__init__.py,sha256=sWTRPTh-qDNkSfpM9rYI3v8zskH8_wFKUuPRg18fZI8,1627
76
76
  schemathesis/generation/case.py,sha256=Rt5MCUtPVYVQzNyjUx8magocPJpHV1svyuqQSTwUE-I,7306
77
- schemathesis/generation/coverage.py,sha256=hyDb465tBoCWE7nI-ZJjhTUzk7f2WDufaadWdSAkdr0,39276
77
+ schemathesis/generation/coverage.py,sha256=YQHmC_wVQ9WBuThNr62LFort9WqHM1DHUtexk-ALSV8,45471
78
78
  schemathesis/generation/meta.py,sha256=36h6m4E7jzLGa8TCvl7eBl_xUWLiRul3qxzexl5cB58,2515
79
79
  schemathesis/generation/modes.py,sha256=t_EvKr2aOXYMsEfdMu4lLF4KCGcX1LVVyvzTkcpJqhk,663
80
80
  schemathesis/generation/overrides.py,sha256=FhqcFoliEvgW6MZyFPYemfLgzKt3Miy8Cud7OMOCb7g,3045
81
81
  schemathesis/generation/targets.py,sha256=_rN2qgxTE2EfvygiN-Fy3WmDnRH0ERohdx3sKRDaYhU,2120
82
82
  schemathesis/generation/hypothesis/__init__.py,sha256=Rl7QwvMBMJI7pBqTydplX6bXC420n0EGQHVm-vZgaYQ,1204
83
- schemathesis/generation/hypothesis/builder.py,sha256=6U4BaTx0VMVRXhmKrEakDSnHunIdEMQiBZfr89pdpP4,29618
83
+ schemathesis/generation/hypothesis/builder.py,sha256=QDfZRpFjQ0KYFPgu2BVSlxop0TQL7fQc201jOMR4rSQ,30472
84
84
  schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
85
85
  schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
86
86
  schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
@@ -99,7 +99,7 @@ schemathesis/pytest/__init__.py,sha256=7W0q-Thcw03IAQfXE_Mo8JPZpUdHJzfu85fjK1Zdf
99
99
  schemathesis/pytest/control_flow.py,sha256=F8rAPsPeNv_sJiJgbZYtTpwKWjauZmqFUaKroY2GmQI,217
100
100
  schemathesis/pytest/lazy.py,sha256=g7DpOeQNsjXC03FCG5e1L65iz3zE48qAyaqG81HzCZY,12028
101
101
  schemathesis/pytest/loaders.py,sha256=oQJ78yyuIm3Ye9X7giVjDB1vYfaW5UY5YuhaTLm_ZFU,266
102
- schemathesis/pytest/plugin.py,sha256=RDOuT25Uotese7W-SD3Pu-nb7zdnaPbyPOoJSkJKSoQ,12379
102
+ schemathesis/pytest/plugin.py,sha256=TxbESQy9JPZBaIwUP4BHiIGFzPd2oMWwq_4VqFS_UfI,12067
103
103
  schemathesis/python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
104
  schemathesis/python/asgi.py,sha256=5PyvuTBaivvyPUEi3pwJni91K1kX5Zc0u9c6c1D8a1Q,287
105
105
  schemathesis/python/wsgi.py,sha256=uShAgo_NChbfYaV1117e6UHp0MTg7jaR0Sy_to3Jmf8,219
@@ -113,28 +113,28 @@ schemathesis/specs/graphql/validation.py,sha256=-W1Noc1MQmTb4RX-gNXMeU2qkgso4mzV
113
113
  schemathesis/specs/openapi/__init__.py,sha256=C5HOsfuDJGq_3mv8CRBvRvb0Diy1p0BFdqyEXMS-loE,238
114
114
  schemathesis/specs/openapi/_cache.py,sha256=HpglmETmZU0RCHxp3DO_sg5_B_nzi54Zuw9vGzzYCxY,4295
115
115
  schemathesis/specs/openapi/_hypothesis.py,sha256=n_39iyz1rt2EdSe-Lyr-3sOIEyJIthnCVR4tGUUvH1c,21328
116
- schemathesis/specs/openapi/checks.py,sha256=m3n5N3_iZcS7inJojW47FF6dfbUQzrBH-bXwsCAOyhM,27737
116
+ schemathesis/specs/openapi/checks.py,sha256=i4tVVkK1wLthdmG-zu7EaQLkBxJ2T3FkuHqw0dA4qlA,27742
117
117
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
118
118
  schemathesis/specs/openapi/converter.py,sha256=lil8IewM5j8tvt4lpA9g_KITvIwx1M96i45DNSHNjoc,3505
119
119
  schemathesis/specs/openapi/definitions.py,sha256=8htclglV3fW6JPBqs59lgM4LnA25Mm9IptXBPb_qUT0,93949
120
120
  schemathesis/specs/openapi/examples.py,sha256=Xvjp60QUcLaeGsJRbi2i6XM15_4uO0ceVoClIaJehiE,21062
121
121
  schemathesis/specs/openapi/formats.py,sha256=ViVF3aFeFI1ctwGQbiRDXhU3so82P0BCaF2aDDbUUm8,2816
122
122
  schemathesis/specs/openapi/media_types.py,sha256=ADedOaNWjbAtAekyaKmNj9fY6zBTeqcNqBEjN0EWNhI,1014
123
- schemathesis/specs/openapi/parameters.py,sha256=hv1reNpSjVuzFbtMpSTwWZ75zcWTOy5ZE0ah6AVEqAo,14565
124
- schemathesis/specs/openapi/patterns.py,sha256=NLnGybcana_kYLVKVEjkEyAzdClAV0xKe4Oy4NVayMI,12834
125
- schemathesis/specs/openapi/references.py,sha256=YjD1xMlaYS7xLt6PrrVS20R72ZWHuFZFTa8Llzf54Rg,8808
126
- schemathesis/specs/openapi/schemas.py,sha256=VSeacEAVJJ6EKJ-llwOaX4aalzUTXyWP8s4wbxTqtWc,54720
123
+ schemathesis/specs/openapi/parameters.py,sha256=tVL61gDe9A8_jwoVKZZvpXKPerMyq7vkAvwdMsi44TI,14622
124
+ schemathesis/specs/openapi/patterns.py,sha256=EQdf4net9QtwngKv36FEr7l0-3_afIMrrBdpKUWGWGc,14382
125
+ schemathesis/specs/openapi/references.py,sha256=c8Ufa8hp6Dyf-gPn5lpmyqF_GtqXIBWoKkj3bk3WaPA,8871
126
+ schemathesis/specs/openapi/schemas.py,sha256=dL1uLz_twgJZUdYBcs2JJ3b8ZlQH3nrGUg1p78pm9Os,55169
127
127
  schemathesis/specs/openapi/security.py,sha256=6UWYMhL-dPtkTineqqBFNKca1i4EuoTduw-EOLeE0aQ,7149
128
128
  schemathesis/specs/openapi/serialization.py,sha256=VdDLmeHqxlWM4cxQQcCkvrU6XurivolwEEaT13ohelA,11972
129
129
  schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
130
130
  schemathesis/specs/openapi/expressions/__init__.py,sha256=hfuRtXD75tQFhzSo6QgDZ3zByyWeZRKevB8edszAVj4,2272
131
131
  schemathesis/specs/openapi/expressions/errors.py,sha256=YLVhps-sYcslgVaahfcUYxUSHlIfWL-rQMeT5PZSMZ8,219
132
132
  schemathesis/specs/openapi/expressions/extractors.py,sha256=Py3of3_vBACP4ljiZIcgd-xQCrWIpcMsfQFc0EtAUoA,470
133
- schemathesis/specs/openapi/expressions/lexer.py,sha256=LeVE6fgYT9-fIsXrv0-YrRHnI4VPisbwsexyh9Q5YU0,3982
134
- schemathesis/specs/openapi/expressions/nodes.py,sha256=YvpbAi8OFdb6RqqrqReGBeADpAmFaoyWN-lGiyYOXTc,4072
133
+ schemathesis/specs/openapi/expressions/lexer.py,sha256=KFA8Z-Kh1IYUpKgwAnDtEucN9YLLpnFR1GQl8KddWlA,3987
134
+ schemathesis/specs/openapi/expressions/nodes.py,sha256=63LC4mQHy3a0_tKiGIVWaUHu9L9IWilq6R004GLpjyY,4077
135
135
  schemathesis/specs/openapi/expressions/parser.py,sha256=e-ZxshrGE_5CVbgcZLYgdGSjdifgyzgKkLQp0dI0cJY,4503
136
136
  schemathesis/specs/openapi/negative/__init__.py,sha256=60QqVBTXPTsAojcf7GDs7v8WbOE_k3g_VC_DBeQUqBw,3749
137
- schemathesis/specs/openapi/negative/mutations.py,sha256=7jTjD9rt5vxWSVBL5Hx8Avj4WhTA63frDQiFMKysrUU,19248
137
+ schemathesis/specs/openapi/negative/mutations.py,sha256=MIFVSWbZHW92KhpWruJT3XLisgc-rFnvYasRtwMmExs,19253
138
138
  schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
139
139
  schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
140
140
  schemathesis/specs/openapi/stateful/__init__.py,sha256=0pu_iGjRiKuqUDN3ewz1zUOt6f1SdvSxVtHC5uK-CYw,14750
@@ -146,8 +146,8 @@ schemathesis/transport/prepare.py,sha256=qQ6zXBw5NN2AIM0bzLAc5Ryc3dmMb0R6xN14lnR
146
146
  schemathesis/transport/requests.py,sha256=j5wI1Uo_PnVuP1eV8l6ddsXosyxAPQ1mLSyWEZmTI9I,8747
147
147
  schemathesis/transport/serialization.py,sha256=jIMra1LqRGav0OX3Hx7mvORt38ll4cd2DKit2D58FN0,10531
148
148
  schemathesis/transport/wsgi.py,sha256=RWSuUXPrl91GxAy8a4jyNNozOWVMRBxKx_tljlWA_Lo,5697
149
- schemathesis-4.0.0a7.dist-info/METADATA,sha256=l_31cA1530vqN9M094EGpnFezX2Sk-YyJDq8_pSWcro,10427
150
- schemathesis-4.0.0a7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
151
- schemathesis-4.0.0a7.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
152
- schemathesis-4.0.0a7.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
153
- schemathesis-4.0.0a7.dist-info/RECORD,,
149
+ schemathesis-4.0.0a9.dist-info/METADATA,sha256=xL00zLDR06C2Jth4cyLvr-lrNBb5mDkognESM0xvJfI,10427
150
+ schemathesis-4.0.0a9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
151
+ schemathesis-4.0.0a9.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
152
+ schemathesis-4.0.0a9.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
153
+ schemathesis-4.0.0a9.dist-info/RECORD,,