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 +4 -0
- schemathesis/core/failures.py +1 -1
- schemathesis/engine/phases/__init__.py +1 -1
- schemathesis/engine/phases/unit/__init__.py +42 -37
- schemathesis/generation/coverage.py +142 -40
- schemathesis/generation/hypothesis/builder.py +32 -5
- schemathesis/hooks.py +1 -1
- schemathesis/pytest/plugin.py +6 -14
- schemathesis/schemas.py +18 -1
- schemathesis/specs/openapi/checks.py +1 -1
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +1 -1
- schemathesis/specs/openapi/negative/mutations.py +1 -1
- schemathesis/specs/openapi/parameters.py +3 -0
- schemathesis/specs/openapi/patterns.py +40 -9
- schemathesis/specs/openapi/references.py +4 -1
- schemathesis/specs/openapi/schemas.py +36 -21
- {schemathesis-4.0.0a7.dist-info → schemathesis-4.0.0a9.dist-info}/METADATA +1 -1
- {schemathesis-4.0.0a7.dist-info → schemathesis-4.0.0a9.dist-info}/RECORD +22 -22
- {schemathesis-4.0.0a7.dist-info → schemathesis-4.0.0a9.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a7.dist-info → schemathesis-4.0.0a9.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a7.dist-info → schemathesis-4.0.0a9.dist-info}/licenses/LICENSE +0 -0
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",
|
schemathesis/core/failures.py
CHANGED
@@ -51,43 +51,48 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
51
51
|
status = None
|
52
52
|
is_executed = False
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
if event
|
75
|
-
status
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
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
|
-
|
418
|
-
|
419
|
-
|
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(
|
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
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
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
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
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
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
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 = (
|
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
|
-
**{
|
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
|
-
|
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
|
-
|
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
schemathesis/pytest/plugin.py
CHANGED
@@ -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
|
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
|
@@ -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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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(
|
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,
|
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
|
-
|
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.
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
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
|
-
|
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,
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
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,
|
1221
|
+
files.append((name, value))
|
1211
1222
|
else:
|
1212
|
-
files.append((name, (None,
|
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.
|
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=
|
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=
|
6
|
+
schemathesis/hooks.py,sha256=ZSGEnsLJ7UVezf4CcaJebVkjEpvwgJolJFZo5fjQNDc,13153
|
7
7
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
schemathesis/schemas.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
124
|
-
schemathesis/specs/openapi/patterns.py,sha256=
|
125
|
-
schemathesis/specs/openapi/references.py,sha256=
|
126
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
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=
|
134
|
-
schemathesis/specs/openapi/expressions/nodes.py,sha256=
|
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=
|
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.
|
150
|
-
schemathesis-4.0.
|
151
|
-
schemathesis-4.0.
|
152
|
-
schemathesis-4.0.
|
153
|
-
schemathesis-4.0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|