schemathesis 4.0.25__py3-none-any.whl → 4.0.26__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/engine/phases/unit/_executor.py +1 -6
- schemathesis/generation/coverage.py +13 -2
- schemathesis/generation/hypothesis/builder.py +41 -7
- schemathesis/generation/meta.py +14 -6
- schemathesis/schemas.py +12 -5
- schemathesis/specs/graphql/schemas.py +7 -21
- schemathesis/specs/openapi/_hypothesis.py +8 -25
- schemathesis/specs/openapi/parameters.py +9 -6
- schemathesis/specs/openapi/schemas.py +10 -64
- schemathesis/specs/openapi/stateful/__init__.py +4 -4
- {schemathesis-4.0.25.dist-info → schemathesis-4.0.26.dist-info}/METADATA +1 -1
- {schemathesis-4.0.25.dist-info → schemathesis-4.0.26.dist-info}/RECORD +15 -17
- schemathesis/specs/graphql/_cache.py +0 -35
- schemathesis/specs/openapi/_cache.py +0 -122
- {schemathesis-4.0.25.dist-info → schemathesis-4.0.26.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.25.dist-info → schemathesis-4.0.26.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.25.dist-info → schemathesis-4.0.26.dist-info}/licenses/LICENSE +0 -0
@@ -263,12 +263,7 @@ def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> No
|
|
263
263
|
|
264
264
|
It increases the effectiveness of the Hypothesis database in the CLI.
|
265
265
|
"""
|
266
|
-
|
267
|
-
# we use all API operation parameters in the digest.
|
268
|
-
extra = operation.label.encode("utf8")
|
269
|
-
for parameter in operation.iter_parameters():
|
270
|
-
extra += parameter.serialize(operation).encode("utf8")
|
271
|
-
test.hypothesis.inner_test._hypothesis_internal_add_digest = extra # type: ignore
|
266
|
+
test.hypothesis.inner_test._hypothesis_internal_add_digest = operation.label.encode("utf8") # type: ignore
|
272
267
|
|
273
268
|
|
274
269
|
def get_invalid_regular_expression_message(warnings: list[WarningMessage]) -> str | None:
|
@@ -321,7 +321,7 @@ class HashSet:
|
|
321
321
|
|
322
322
|
|
323
323
|
def _cover_positive_for_type(
|
324
|
-
ctx: CoverageContext, schema: dict, ty: str | None
|
324
|
+
ctx: CoverageContext, schema: dict, ty: str | None, seen: HashSet | None = None
|
325
325
|
) -> Generator[GeneratedValue, None, None]:
|
326
326
|
if ty == "object" or ty == "array":
|
327
327
|
template_schema = _get_template_schema(schema, ty)
|
@@ -369,6 +369,9 @@ def _cover_positive_for_type(
|
|
369
369
|
yield from _positive_object(ctx, schema, cast(dict, template))
|
370
370
|
elif "properties" in schema or "required" in schema:
|
371
371
|
yield from _positive_object(ctx, schema, cast(dict, template))
|
372
|
+
elif "not" in schema and isinstance(schema["not"], (dict, bool)):
|
373
|
+
nctx = ctx.with_negative()
|
374
|
+
yield from cover_schema_iter(nctx, schema["not"], seen)
|
372
375
|
|
373
376
|
|
374
377
|
@contextmanager
|
@@ -397,9 +400,12 @@ def cover_schema_iter(
|
|
397
400
|
) -> Generator[GeneratedValue, None, None]:
|
398
401
|
if seen is None:
|
399
402
|
seen = HashSet()
|
400
|
-
if
|
403
|
+
if schema == {} or schema is True:
|
401
404
|
types = ["null", "boolean", "string", "number", "array", "object"]
|
402
405
|
schema = {}
|
406
|
+
elif schema is False:
|
407
|
+
types = []
|
408
|
+
schema = {"not": {}}
|
403
409
|
else:
|
404
410
|
types = schema.get("type", [])
|
405
411
|
push_examples_to_properties(schema)
|
@@ -615,6 +621,9 @@ def cover_schema_iter(
|
|
615
621
|
for value in cover_schema_iter(nctx, sub_schema, seen):
|
616
622
|
if is_invalid_for_oneOf(value.value, idx, validators):
|
617
623
|
yield value
|
624
|
+
elif key == "not" and isinstance(value, (dict, bool)):
|
625
|
+
pctx = ctx.with_positive()
|
626
|
+
yield from cover_schema_iter(pctx, value, seen)
|
618
627
|
|
619
628
|
|
620
629
|
def is_valid_for_others(value: Any, idx: int, validators: list[jsonschema.Validator]) -> bool:
|
@@ -690,6 +699,7 @@ def _ensure_valid_headers_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
|
690
699
|
def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
691
700
|
"""Generate positive string values."""
|
692
701
|
# Boundary and near boundary values
|
702
|
+
schema = {"type": "string", **schema}
|
693
703
|
min_length = schema.get("minLength")
|
694
704
|
if min_length == 0:
|
695
705
|
min_length = None
|
@@ -791,6 +801,7 @@ def closest_multiple_greater_than(y: int, x: int) -> int:
|
|
791
801
|
def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
792
802
|
"""Generate positive integer values."""
|
793
803
|
# Boundary and near boundary values
|
804
|
+
schema = {"type": "number", **schema}
|
794
805
|
minimum = schema.get("minimum")
|
795
806
|
maximum = schema.get("maximum")
|
796
807
|
exclusive_minimum = schema.get("exclusiveMinimum")
|
@@ -234,6 +234,22 @@ def add_examples(
|
|
234
234
|
hook_dispatcher: HookDispatcher | None = None,
|
235
235
|
**kwargs: Any,
|
236
236
|
) -> Callable:
|
237
|
+
for example in generate_example_cases(
|
238
|
+
test=test, operation=operation, fill_missing=fill_missing, hook_dispatcher=hook_dispatcher, **kwargs
|
239
|
+
):
|
240
|
+
test = hypothesis.example(case=example)(test)
|
241
|
+
|
242
|
+
return test
|
243
|
+
|
244
|
+
|
245
|
+
def generate_example_cases(
|
246
|
+
*,
|
247
|
+
test: Callable,
|
248
|
+
operation: APIOperation,
|
249
|
+
fill_missing: bool,
|
250
|
+
hook_dispatcher: HookDispatcher | None = None,
|
251
|
+
**kwargs: Any,
|
252
|
+
) -> Generator[Case]:
|
237
253
|
"""Add examples to the Hypothesis test, if they are specified in the schema."""
|
238
254
|
from hypothesis_jsonschema._canonicalise import HypothesisRefResolutionError
|
239
255
|
|
@@ -273,9 +289,7 @@ def add_examples(
|
|
273
289
|
InvalidHeadersExampleMark.set(original_test, invalid_headers)
|
274
290
|
continue
|
275
291
|
adjust_urlencoded_payload(example)
|
276
|
-
|
277
|
-
|
278
|
-
return test
|
292
|
+
yield example
|
279
293
|
|
280
294
|
|
281
295
|
def adjust_urlencoded_payload(case: Case) -> None:
|
@@ -298,6 +312,29 @@ def add_coverage(
|
|
298
312
|
unexpected_methods: set[str],
|
299
313
|
generation_config: GenerationConfig,
|
300
314
|
) -> Callable:
|
315
|
+
for case in generate_coverage_cases(
|
316
|
+
operation=operation,
|
317
|
+
generation_modes=generation_modes,
|
318
|
+
auth_storage=auth_storage,
|
319
|
+
as_strategy_kwargs=as_strategy_kwargs,
|
320
|
+
generate_duplicate_query_parameters=generate_duplicate_query_parameters,
|
321
|
+
unexpected_methods=unexpected_methods,
|
322
|
+
generation_config=generation_config,
|
323
|
+
):
|
324
|
+
test = hypothesis.example(case=case)(test)
|
325
|
+
return test
|
326
|
+
|
327
|
+
|
328
|
+
def generate_coverage_cases(
|
329
|
+
*,
|
330
|
+
operation: APIOperation,
|
331
|
+
generation_modes: list[GenerationMode],
|
332
|
+
auth_storage: AuthStorage | None,
|
333
|
+
as_strategy_kwargs: dict[str, Any],
|
334
|
+
generate_duplicate_query_parameters: bool,
|
335
|
+
unexpected_methods: set[str],
|
336
|
+
generation_config: GenerationConfig,
|
337
|
+
) -> Generator[Case]:
|
301
338
|
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
302
339
|
|
303
340
|
auth_context = auths.AuthContext(
|
@@ -309,7 +346,6 @@ def add_coverage(
|
|
309
346
|
for container in LOCATION_TO_CONTAINER.values()
|
310
347
|
if container in as_strategy_kwargs
|
311
348
|
}
|
312
|
-
|
313
349
|
with warnings.catch_warnings():
|
314
350
|
warnings.filterwarnings(
|
315
351
|
"ignore", message=".*but this is not valid syntax for a Python regular expression.*", category=UserWarning
|
@@ -331,9 +367,7 @@ def add_coverage(
|
|
331
367
|
setattr(case, container_name, value)
|
332
368
|
else:
|
333
369
|
container.update(value)
|
334
|
-
|
335
|
-
test = hypothesis.example(case=case)(test)
|
336
|
-
return test
|
370
|
+
yield case
|
337
371
|
|
338
372
|
|
339
373
|
class Instant:
|
schemathesis/generation/meta.py
CHANGED
@@ -12,6 +12,7 @@ class TestPhase(str, Enum):
|
|
12
12
|
EXAMPLES = "examples"
|
13
13
|
COVERAGE = "coverage"
|
14
14
|
FUZZING = "fuzzing"
|
15
|
+
STATEFUL = "stateful"
|
15
16
|
|
16
17
|
|
17
18
|
class ComponentKind(str, Enum):
|
@@ -34,15 +35,22 @@ class ComponentInfo:
|
|
34
35
|
|
35
36
|
|
36
37
|
@dataclass
|
37
|
-
class
|
38
|
+
class FuzzingPhaseData:
|
38
39
|
"""Metadata specific to generate phase."""
|
39
40
|
|
40
41
|
__slots__ = ()
|
41
42
|
|
42
43
|
|
43
44
|
@dataclass
|
44
|
-
class
|
45
|
-
"""Metadata specific to
|
45
|
+
class StatefulPhaseData:
|
46
|
+
"""Metadata specific to stateful phase."""
|
47
|
+
|
48
|
+
__slots__ = ()
|
49
|
+
|
50
|
+
|
51
|
+
@dataclass
|
52
|
+
class ExamplesPhaseData:
|
53
|
+
"""Metadata specific to examples phase."""
|
46
54
|
|
47
55
|
__slots__ = ()
|
48
56
|
|
@@ -64,7 +72,7 @@ class PhaseInfo:
|
|
64
72
|
"""Phase-specific information."""
|
65
73
|
|
66
74
|
name: TestPhase
|
67
|
-
data: CoveragePhaseData |
|
75
|
+
data: CoveragePhaseData | ExamplesPhaseData | FuzzingPhaseData | StatefulPhaseData
|
68
76
|
|
69
77
|
__slots__ = ("name", "data")
|
70
78
|
|
@@ -84,8 +92,8 @@ class PhaseInfo:
|
|
84
92
|
)
|
85
93
|
|
86
94
|
@classmethod
|
87
|
-
def
|
88
|
-
return cls(name=TestPhase.FUZZING, data=
|
95
|
+
def fuzzing(cls) -> PhaseInfo:
|
96
|
+
return cls(name=TestPhase.FUZZING, data=FuzzingPhaseData())
|
89
97
|
|
90
98
|
|
91
99
|
@dataclass
|
schemathesis/schemas.py
CHANGED
@@ -545,10 +545,6 @@ class Parameter:
|
|
545
545
|
"""Whether the parameter is required for a successful API call."""
|
546
546
|
raise NotImplementedError
|
547
547
|
|
548
|
-
def serialize(self, operation: APIOperation) -> str:
|
549
|
-
"""Get parameter's string representation."""
|
550
|
-
raise NotImplementedError
|
551
|
-
|
552
548
|
|
553
549
|
P = TypeVar("P", bound=Parameter)
|
554
550
|
|
@@ -620,7 +616,7 @@ class OperationDefinition(Generic[D]):
|
|
620
616
|
def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
|
621
617
|
|
622
618
|
|
623
|
-
@dataclass(
|
619
|
+
@dataclass()
|
624
620
|
class APIOperation(Generic[P]):
|
625
621
|
"""An API operation (e.g., `GET /users`)."""
|
626
622
|
|
@@ -647,6 +643,14 @@ class APIOperation(Generic[P]):
|
|
647
643
|
def __deepcopy__(self, memo: dict) -> APIOperation[P]:
|
648
644
|
return self
|
649
645
|
|
646
|
+
def __hash__(self) -> int:
|
647
|
+
return hash(self.label)
|
648
|
+
|
649
|
+
def __eq__(self, value: object, /) -> bool:
|
650
|
+
if not isinstance(value, APIOperation):
|
651
|
+
return NotImplemented
|
652
|
+
return self.label == value.label
|
653
|
+
|
650
654
|
@property
|
651
655
|
def full_path(self) -> str:
|
652
656
|
return self.schema.get_full_path(self.path)
|
@@ -700,6 +704,9 @@ class APIOperation(Generic[P]):
|
|
700
704
|
**kwargs: Extra arguments to the underlying strategy function.
|
701
705
|
|
702
706
|
"""
|
707
|
+
if self.schema.config.headers:
|
708
|
+
headers = kwargs.setdefault("headers", {})
|
709
|
+
headers.update(self.schema.config.headers)
|
703
710
|
strategy = self.schema.get_case_strategy(self, generation_mode=generation_mode, **kwargs)
|
704
711
|
|
705
712
|
def _apply_hooks(dispatcher: HookDispatcher, _strategy: SearchStrategy[Case]) -> SearchStrategy[Case]:
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import enum
|
4
4
|
import time
|
5
|
-
from dataclasses import dataclass
|
5
|
+
from dataclasses import dataclass
|
6
6
|
from difflib import get_close_matches
|
7
7
|
from enum import unique
|
8
8
|
from types import SimpleNamespace
|
@@ -34,8 +34,8 @@ from schemathesis.generation.meta import (
|
|
34
34
|
CaseMetadata,
|
35
35
|
ComponentInfo,
|
36
36
|
ComponentKind,
|
37
|
-
|
38
|
-
|
37
|
+
ExamplesPhaseData,
|
38
|
+
FuzzingPhaseData,
|
39
39
|
GenerationInfo,
|
40
40
|
PhaseInfo,
|
41
41
|
TestPhase,
|
@@ -50,7 +50,6 @@ from schemathesis.schemas import (
|
|
50
50
|
)
|
51
51
|
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
52
52
|
|
53
|
-
from ._cache import OperationCache
|
54
53
|
from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
|
55
54
|
|
56
55
|
if TYPE_CHECKING:
|
@@ -86,8 +85,6 @@ class GraphQLOperationDefinition(OperationDefinition):
|
|
86
85
|
|
87
86
|
@dataclass
|
88
87
|
class GraphQLSchema(BaseSchema):
|
89
|
-
_operation_cache: OperationCache = field(default_factory=OperationCache)
|
90
|
-
|
91
88
|
def __repr__(self) -> str:
|
92
89
|
return f"<{self.__class__.__name__}>"
|
93
90
|
|
@@ -101,10 +98,6 @@ class GraphQLSchema(BaseSchema):
|
|
101
98
|
yield operation_type.name
|
102
99
|
|
103
100
|
def _get_operation_map(self, key: str) -> APIOperationMap:
|
104
|
-
cache = self._operation_cache
|
105
|
-
map = cache.get_map(key)
|
106
|
-
if map is not None:
|
107
|
-
return map
|
108
101
|
schema = self.client_schema
|
109
102
|
for root_type, operation_type in (
|
110
103
|
(RootType.QUERY, schema.query_type),
|
@@ -113,7 +106,6 @@ class GraphQLSchema(BaseSchema):
|
|
113
106
|
if operation_type and operation_type.name == key:
|
114
107
|
map = APIOperationMap(self, {})
|
115
108
|
map._data = FieldMap(map, root_type, operation_type)
|
116
|
-
cache.insert_map(key, map)
|
117
109
|
return map
|
118
110
|
raise KeyError(key)
|
119
111
|
|
@@ -303,15 +295,9 @@ class FieldMap(Mapping):
|
|
303
295
|
|
304
296
|
def _init_operation(self, field_name: str) -> APIOperation:
|
305
297
|
schema = cast(GraphQLSchema, self._parent._schema)
|
306
|
-
cache = schema._operation_cache
|
307
|
-
operation = cache.get_operation(field_name)
|
308
|
-
if operation is not None:
|
309
|
-
return operation
|
310
298
|
operation_type = self._operation_type
|
311
299
|
field_ = operation_type.fields[field_name]
|
312
|
-
|
313
|
-
cache.insert_operation(field_name, operation)
|
314
|
-
return operation
|
300
|
+
return schema._build_operation(self._root_type, operation_type, field_name, field_)
|
315
301
|
|
316
302
|
def __getitem__(self, item: str) -> APIOperation:
|
317
303
|
try:
|
@@ -368,10 +354,10 @@ def graphql_cases(
|
|
368
354
|
query_ = _generate_parameter("query", query, draw, operation, hook_context, hooks)
|
369
355
|
|
370
356
|
_phase_data = {
|
371
|
-
TestPhase.EXAMPLES:
|
372
|
-
TestPhase.FUZZING:
|
357
|
+
TestPhase.EXAMPLES: ExamplesPhaseData(),
|
358
|
+
TestPhase.FUZZING: FuzzingPhaseData(),
|
373
359
|
}[phase]
|
374
|
-
phase_data = cast(Union[
|
360
|
+
phase_data = cast(Union[ExamplesPhaseData, FuzzingPhaseData], _phase_data)
|
375
361
|
instance = operation.Case(
|
376
362
|
path_parameters=path_parameters_,
|
377
363
|
headers=headers_,
|
@@ -5,7 +5,6 @@ from contextlib import suppress
|
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from typing import Any, Callable, Dict, Iterable, Optional, Union, cast
|
7
7
|
from urllib.parse import quote_plus
|
8
|
-
from weakref import WeakKeyDictionary
|
9
8
|
|
10
9
|
import jsonschema.protocols
|
11
10
|
from hypothesis import event, note, reject
|
@@ -23,10 +22,11 @@ from schemathesis.generation.meta import (
|
|
23
22
|
CaseMetadata,
|
24
23
|
ComponentInfo,
|
25
24
|
ComponentKind,
|
26
|
-
|
27
|
-
|
25
|
+
ExamplesPhaseData,
|
26
|
+
FuzzingPhaseData,
|
28
27
|
GenerationInfo,
|
29
28
|
PhaseInfo,
|
29
|
+
StatefulPhaseData,
|
30
30
|
TestPhase,
|
31
31
|
)
|
32
32
|
from schemathesis.openapi.generation.filters import is_valid_header, is_valid_path, is_valid_query, is_valid_urlencoded
|
@@ -64,7 +64,6 @@ def openapi_cases(
|
|
64
64
|
body: Any = NOT_SET,
|
65
65
|
media_type: str | None = None,
|
66
66
|
phase: TestPhase = TestPhase.FUZZING,
|
67
|
-
__is_stateful_phase: bool = False,
|
68
67
|
) -> Any:
|
69
68
|
"""A strategy that creates `Case` instances.
|
70
69
|
|
@@ -81,8 +80,7 @@ def openapi_cases(
|
|
81
80
|
start = time.monotonic()
|
82
81
|
strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
|
83
82
|
|
84
|
-
|
85
|
-
generation_config = operation.schema.config.generation_for(operation=operation, phase=phase_name)
|
83
|
+
generation_config = operation.schema.config.generation_for(operation=operation, phase=phase.value)
|
86
84
|
|
87
85
|
ctx = HookContext(operation=operation)
|
88
86
|
|
@@ -151,10 +149,11 @@ def openapi_cases(
|
|
151
149
|
reject()
|
152
150
|
|
153
151
|
_phase_data = {
|
154
|
-
TestPhase.EXAMPLES:
|
155
|
-
TestPhase.FUZZING:
|
152
|
+
TestPhase.EXAMPLES: ExamplesPhaseData(),
|
153
|
+
TestPhase.FUZZING: FuzzingPhaseData(),
|
154
|
+
TestPhase.STATEFUL: StatefulPhaseData(),
|
156
155
|
}[phase]
|
157
|
-
phase_data = cast(Union[
|
156
|
+
phase_data = cast(Union[ExamplesPhaseData, FuzzingPhaseData, StatefulPhaseData], _phase_data)
|
158
157
|
|
159
158
|
instance = operation.Case(
|
160
159
|
media_type=media_type,
|
@@ -190,9 +189,6 @@ def openapi_cases(
|
|
190
189
|
return instance
|
191
190
|
|
192
191
|
|
193
|
-
_BODY_STRATEGIES_CACHE: WeakKeyDictionary = WeakKeyDictionary()
|
194
|
-
|
195
|
-
|
196
192
|
def _get_body_strategy(
|
197
193
|
parameter: OpenAPIBody,
|
198
194
|
strategy_factory: StrategyFactory,
|
@@ -203,10 +199,6 @@ def _get_body_strategy(
|
|
203
199
|
|
204
200
|
if parameter.media_type in MEDIA_TYPES:
|
205
201
|
return MEDIA_TYPES[parameter.media_type]
|
206
|
-
# The cache key relies on object ids, which means that the parameter should not be mutated
|
207
|
-
# Note, the parent schema is not included as each parameter belong only to one schema
|
208
|
-
if parameter in _BODY_STRATEGIES_CACHE and strategy_factory in _BODY_STRATEGIES_CACHE[parameter]:
|
209
|
-
return _BODY_STRATEGIES_CACHE[parameter][strategy_factory]
|
210
202
|
schema = parameter.as_json_schema(operation)
|
211
203
|
schema = operation.schema.prepare_schema(schema)
|
212
204
|
assert isinstance(operation.schema, BaseOpenAPISchema)
|
@@ -215,7 +207,6 @@ def _get_body_strategy(
|
|
215
207
|
)
|
216
208
|
if not parameter.is_required:
|
217
209
|
strategy |= st.just(NOT_SET)
|
218
|
-
_BODY_STRATEGIES_CACHE.setdefault(parameter, {})[strategy_factory] = strategy
|
219
210
|
return strategy
|
220
211
|
|
221
212
|
|
@@ -248,9 +239,6 @@ def get_parameters_value(
|
|
248
239
|
return value
|
249
240
|
|
250
241
|
|
251
|
-
_PARAMETER_STRATEGIES_CACHE: WeakKeyDictionary = WeakKeyDictionary()
|
252
|
-
|
253
|
-
|
254
242
|
@dataclass
|
255
243
|
class ValueContainer:
|
256
244
|
"""Container for a value generated by a data generator or explicitly provided."""
|
@@ -352,10 +340,6 @@ def get_parameters_strategy(
|
|
352
340
|
|
353
341
|
parameters = getattr(operation, LOCATION_TO_CONTAINER[location])
|
354
342
|
if parameters:
|
355
|
-
# The cache key relies on object ids, which means that the parameter should not be mutated
|
356
|
-
nested_cache_key = (strategy_factory, location, tuple(sorted(exclude)))
|
357
|
-
if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
|
358
|
-
return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
|
359
343
|
schema = get_schema_for_location(operation, location, parameters)
|
360
344
|
if location == "header" and exclude:
|
361
345
|
# Remove excluded headers case-insensitively
|
@@ -399,7 +383,6 @@ def get_parameters_strategy(
|
|
399
383
|
strategy = strategy.map(quote_all).map(jsonify_python_specific_types)
|
400
384
|
elif location == "query":
|
401
385
|
strategy = strategy.map(jsonify_python_specific_types)
|
402
|
-
_PARAMETER_STRATEGIES_CACHE.setdefault(operation, {})[nested_cache_key] = strategy
|
403
386
|
return strategy
|
404
387
|
# No parameters defined for this location
|
405
388
|
return st.none()
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import json
|
4
3
|
from dataclasses import dataclass
|
5
4
|
from typing import TYPE_CHECKING, Any, ClassVar, Iterable
|
6
5
|
|
@@ -95,11 +94,6 @@ class OpenAPIParameter(Parameter):
|
|
95
94
|
if key in self.supported_jsonschema_keywords or key.startswith("x-") or key == self.nullable_field
|
96
95
|
}
|
97
96
|
|
98
|
-
def serialize(self, operation: APIOperation) -> str:
|
99
|
-
# For simplicity, JSON Schema semantics is not taken into account (e.g. 1 == 1.0)
|
100
|
-
# I.e. two semantically equal schemas may have different representation
|
101
|
-
return json.dumps(self.as_json_schema(operation), sort_keys=True)
|
102
|
-
|
103
97
|
|
104
98
|
@dataclass(eq=False)
|
105
99
|
class OpenAPI20Parameter(OpenAPIParameter):
|
@@ -170,6 +164,7 @@ class OpenAPI30Parameter(OpenAPIParameter):
|
|
170
164
|
"minProperties",
|
171
165
|
"required",
|
172
166
|
"enum",
|
167
|
+
"const",
|
173
168
|
"type",
|
174
169
|
"allOf",
|
175
170
|
"oneOf",
|
@@ -178,6 +173,14 @@ class OpenAPI30Parameter(OpenAPIParameter):
|
|
178
173
|
"items",
|
179
174
|
"properties",
|
180
175
|
"additionalProperties",
|
176
|
+
"additionalItems",
|
177
|
+
"dependencies",
|
178
|
+
"if",
|
179
|
+
"then",
|
180
|
+
"else",
|
181
|
+
"patternProperties",
|
182
|
+
"propertyNames",
|
183
|
+
"contains",
|
181
184
|
"format",
|
182
185
|
"example",
|
183
186
|
"examples",
|
@@ -47,7 +47,6 @@ from ...generation import GenerationMode
|
|
47
47
|
from ...hooks import HookContext, HookDispatcher
|
48
48
|
from ...schemas import APIOperation, APIOperationMap, ApiStatistic, BaseSchema, OperationDefinition
|
49
49
|
from . import serialization
|
50
|
-
from ._cache import OperationCache
|
51
50
|
from ._hypothesis import openapi_cases
|
52
51
|
from .converter import to_json_schema, to_json_schema_recursive
|
53
52
|
from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
|
@@ -96,7 +95,6 @@ class BaseOpenAPISchema(BaseSchema):
|
|
96
95
|
links_field: ClassVar[str] = ""
|
97
96
|
header_required_field: ClassVar[str] = ""
|
98
97
|
security: ClassVar[BaseSecurityProcessor] = None # type: ignore
|
99
|
-
_operation_cache: OperationCache = field(default_factory=OperationCache)
|
100
98
|
_inline_reference_cache: dict[str, Any] = field(default_factory=dict)
|
101
99
|
# Inline references cache can be populated from multiple threads, therefore we need some synchronisation to avoid
|
102
100
|
# excessive resolving
|
@@ -115,17 +113,12 @@ class BaseOpenAPISchema(BaseSchema):
|
|
115
113
|
return iter(self.raw_schema.get("paths", {}))
|
116
114
|
|
117
115
|
def _get_operation_map(self, path: str) -> APIOperationMap:
|
118
|
-
cache = self._operation_cache
|
119
|
-
map = cache.get_map(path)
|
120
|
-
if map is not None:
|
121
|
-
return map
|
122
116
|
path_item = self.raw_schema.get("paths", {})[path]
|
123
117
|
with in_scope(self.resolver, self.location or ""):
|
124
118
|
scope, path_item = self._resolve_path_item(path_item)
|
125
119
|
self.dispatch_hook("before_process_path", HookContext(), path, path_item)
|
126
120
|
map = APIOperationMap(self, {})
|
127
121
|
map._data = MethodMap(map, scope, path, CaseInsensitiveDict(path_item))
|
128
|
-
cache.insert_map(path, map)
|
129
122
|
return map
|
130
123
|
|
131
124
|
def find_operation_by_label(self, label: str) -> APIOperation | None:
|
@@ -136,7 +129,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
136
129
|
matches = get_close_matches(item, list(self))
|
137
130
|
self._on_missing_operation(item, exc, matches)
|
138
131
|
|
139
|
-
def _on_missing_operation(self, item: str, exc: KeyError, matches: list[str]) -> NoReturn:
|
132
|
+
def _on_missing_operation(self, item: str, exc: KeyError | None, matches: list[str]) -> NoReturn:
|
140
133
|
message = f"`{item}` not found"
|
141
134
|
if matches:
|
142
135
|
message += f". Did you mean `{matches[0]}`?"
|
@@ -443,31 +436,6 @@ class BaseOpenAPISchema(BaseSchema):
|
|
443
436
|
|
444
437
|
def get_operation_by_id(self, operation_id: str) -> APIOperation:
|
445
438
|
"""Get an `APIOperation` instance by its `operationId`."""
|
446
|
-
cache = self._operation_cache
|
447
|
-
cached = cache.get_operation_by_id(operation_id)
|
448
|
-
if cached is not None:
|
449
|
-
return cached
|
450
|
-
# Operation has not been accessed yet, need to populate the cache
|
451
|
-
if not cache.has_ids_to_definitions:
|
452
|
-
self._populate_operation_id_cache(cache)
|
453
|
-
try:
|
454
|
-
entry = cache.get_definition_by_id(operation_id)
|
455
|
-
except KeyError as exc:
|
456
|
-
matches = get_close_matches(operation_id, cache.known_operation_ids)
|
457
|
-
self._on_missing_operation(operation_id, exc, matches)
|
458
|
-
# It could've been already accessed in a different place
|
459
|
-
traversal_key = (entry.scope, entry.path, entry.method)
|
460
|
-
instance = cache.get_operation_by_traversal_key(traversal_key)
|
461
|
-
if instance is not None:
|
462
|
-
return instance
|
463
|
-
resolved = self._resolve_operation(entry.operation)
|
464
|
-
parameters = self._collect_operation_parameters(entry.path_item, resolved)
|
465
|
-
initialized = self.make_operation(entry.path, entry.method, parameters, entry.operation, resolved, entry.scope)
|
466
|
-
cache.insert_operation(initialized, traversal_key=traversal_key, operation_id=operation_id)
|
467
|
-
return initialized
|
468
|
-
|
469
|
-
def _populate_operation_id_cache(self, cache: OperationCache) -> None:
|
470
|
-
"""Collect all operation IDs from the schema."""
|
471
439
|
resolve = self.resolver.resolve
|
472
440
|
default_scope = self.resolver.resolution_scope
|
473
441
|
for path, path_item in self.raw_schema.get("paths", {}).items():
|
@@ -477,44 +445,29 @@ class BaseOpenAPISchema(BaseSchema):
|
|
477
445
|
scope, path_item = resolve(path_item["$ref"])
|
478
446
|
else:
|
479
447
|
scope = default_scope
|
480
|
-
for
|
481
|
-
if
|
448
|
+
for method, operation in path_item.items():
|
449
|
+
if method not in HTTP_METHODS:
|
482
450
|
continue
|
483
|
-
if "operationId" in
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
scope=scope,
|
489
|
-
path_item=path_item,
|
490
|
-
operation=entry,
|
491
|
-
)
|
451
|
+
if "operationId" in operation and operation["operationId"] == operation_id:
|
452
|
+
resolved = self._resolve_operation(operation)
|
453
|
+
parameters = self._collect_operation_parameters(path_item, resolved)
|
454
|
+
return self.make_operation(path, method, parameters, operation, resolved, scope)
|
455
|
+
self._on_missing_operation(operation_id, None, [])
|
492
456
|
|
493
457
|
def get_operation_by_reference(self, reference: str) -> APIOperation:
|
494
458
|
"""Get local or external `APIOperation` instance by reference.
|
495
459
|
|
496
460
|
Reference example: #/paths/~1users~1{user_id}/patch
|
497
461
|
"""
|
498
|
-
cache = self._operation_cache
|
499
|
-
cached = cache.get_operation_by_reference(reference)
|
500
|
-
if cached is not None:
|
501
|
-
return cached
|
502
462
|
scope, operation = self.resolver.resolve(reference)
|
503
463
|
path, method = scope.rsplit("/", maxsplit=2)[-2:]
|
504
464
|
path = path.replace("~1", "/").replace("~0", "~")
|
505
|
-
# Check the traversal cache as it could've been populated in other places
|
506
|
-
traversal_key = (self.resolver.resolution_scope, path, method)
|
507
|
-
cached = cache.get_operation_by_traversal_key(traversal_key)
|
508
|
-
if cached is not None:
|
509
|
-
return cached
|
510
465
|
with in_scope(self.resolver, scope):
|
511
466
|
resolved = self._resolve_operation(operation)
|
512
467
|
parent_ref, _ = reference.rsplit("/", maxsplit=1)
|
513
468
|
_, path_item = self.resolver.resolve(parent_ref)
|
514
469
|
parameters = self._collect_operation_parameters(path_item, resolved)
|
515
|
-
|
516
|
-
cache.insert_operation(initialized, traversal_key=traversal_key, reference=reference)
|
517
|
-
return initialized
|
470
|
+
return self.make_operation(path, method, parameters, operation, resolved, scope)
|
518
471
|
|
519
472
|
def get_case_strategy(
|
520
473
|
self,
|
@@ -851,22 +804,15 @@ class MethodMap(Mapping):
|
|
851
804
|
method = method.lower()
|
852
805
|
operation = self._path_item[method]
|
853
806
|
schema = cast(BaseOpenAPISchema, self._parent._schema)
|
854
|
-
cache = schema._operation_cache
|
855
807
|
path = self._path
|
856
808
|
scope = self._scope
|
857
|
-
traversal_key = (scope, path, method)
|
858
|
-
cached = cache.get_operation_by_traversal_key(traversal_key)
|
859
|
-
if cached is not None:
|
860
|
-
return cached
|
861
809
|
schema.resolver.push_scope(scope)
|
862
810
|
try:
|
863
811
|
resolved = schema._resolve_operation(operation)
|
864
812
|
finally:
|
865
813
|
schema.resolver.pop_scope()
|
866
814
|
parameters = schema._collect_operation_parameters(self._path_item, resolved)
|
867
|
-
|
868
|
-
cache.insert_operation(initialized, traversal_key=traversal_key, operation_id=resolved.get("operationId"))
|
869
|
-
return initialized
|
815
|
+
return schema.make_operation(path, method, parameters, operation, resolved, scope)
|
870
816
|
|
871
817
|
def __getitem__(self, item: str) -> APIOperation:
|
872
818
|
try:
|
@@ -15,7 +15,7 @@ from schemathesis.engine.recorder import ScenarioRecorder
|
|
15
15
|
from schemathesis.generation import GenerationMode
|
16
16
|
from schemathesis.generation.case import Case
|
17
17
|
from schemathesis.generation.hypothesis import strategies
|
18
|
-
from schemathesis.generation.meta import ComponentInfo, ComponentKind
|
18
|
+
from schemathesis.generation.meta import ComponentInfo, ComponentKind, TestPhase
|
19
19
|
from schemathesis.generation.stateful import STATEFUL_TESTS_LABEL
|
20
20
|
from schemathesis.generation.stateful.state_machine import APIStateMachine, StepInput, StepOutput, _normalize_name
|
21
21
|
from schemathesis.schemas import APIOperation
|
@@ -179,10 +179,10 @@ def create_state_machine(schema: BaseOpenAPISchema) -> type[APIStateMachine]:
|
|
179
179
|
name = _normalize_name(f"RANDOM -> {target.label}")
|
180
180
|
config = schema.config.generation_for(operation=target, phase="stateful")
|
181
181
|
if len(config.modes) == 1:
|
182
|
-
case_strategy = target.as_strategy(generation_mode=config.modes[0],
|
182
|
+
case_strategy = target.as_strategy(generation_mode=config.modes[0], phase=TestPhase.STATEFUL)
|
183
183
|
else:
|
184
184
|
_strategies = {
|
185
|
-
method: target.as_strategy(generation_mode=method,
|
185
|
+
method: target.as_strategy(generation_mode=method, phase=TestPhase.STATEFUL)
|
186
186
|
for method in config.modes
|
187
187
|
}
|
188
188
|
|
@@ -269,7 +269,7 @@ def into_step_input(
|
|
269
269
|
):
|
270
270
|
kwargs["body"] = transition.request_body.value.ok()
|
271
271
|
cases = strategies.combine(
|
272
|
-
[target.as_strategy(generation_mode=mode,
|
272
|
+
[target.as_strategy(generation_mode=mode, phase=TestPhase.STATEFUL, **kwargs) for mode in modes]
|
273
273
|
)
|
274
274
|
case = draw(cases)
|
275
275
|
if (
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: schemathesis
|
3
|
-
Version: 4.0.
|
3
|
+
Version: 4.0.26
|
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://github.com/schemathesis/schemathesis/blob/master/CHANGELOG.md
|
@@ -5,7 +5,7 @@ schemathesis/errors.py,sha256=T8nobEi5tQX_SkwaYb8JFoIlF9F_vOQVprZ8EVPAhjA,931
|
|
5
5
|
schemathesis/filters.py,sha256=OEub50Ob5sf0Tn3iTeuIaxSMtepF7KVoiEM9wtt5GGA,13433
|
6
6
|
schemathesis/hooks.py,sha256=OEnZw5lKSweNdycBRIOsn085rTB-L-iJAjj4ClldkU4,13932
|
7
7
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
schemathesis/schemas.py,sha256=
|
8
|
+
schemathesis/schemas.py,sha256=thyBKI50sJsj7PE52GvLy9_ktPMDP-fbLM8ZwvdHuD0,28618
|
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=CVcQNHEiX-joAQmyuEVKWPOSxDHsOw_EXXZsEclzLuY,341
|
@@ -81,17 +81,17 @@ schemathesis/engine/phases/stateful/__init__.py,sha256=Lz1rgNqCfUSIz173XqCGsiMuU
|
|
81
81
|
schemathesis/engine/phases/stateful/_executor.py,sha256=_303Yqflx1iFNTQI2EfjSp_2T21YvzJJgMSazhpv5JQ,15200
|
82
82
|
schemathesis/engine/phases/stateful/context.py,sha256=A7X1SLDOWFpCvFN9IiIeNVZM0emjqatmJL_k9UsO7vM,2946
|
83
83
|
schemathesis/engine/phases/unit/__init__.py,sha256=BvZh39LZmXg90Cy_Tn0cQY5y7eWzYvAEmJ43fGKFAt8,8715
|
84
|
-
schemathesis/engine/phases/unit/_executor.py,sha256=
|
84
|
+
schemathesis/engine/phases/unit/_executor.py,sha256=tRv_QjwtXOuSZCl_j-S_EnhcrKScr9gyGGZItzg16XI,16190
|
85
85
|
schemathesis/engine/phases/unit/_pool.py,sha256=iU0hdHDmohPnEv7_S1emcabuzbTf-Cznqwn0pGQ5wNQ,2480
|
86
86
|
schemathesis/generation/__init__.py,sha256=tvNO2FLiY8z3fZ_kL_QJhSgzXfnT4UqwSXMHCwfLI0g,645
|
87
87
|
schemathesis/generation/case.py,sha256=zwAwFQ-Fp7SOxCXYOQyAdwAtNwVJe63PdLpvqackFQY,12296
|
88
|
-
schemathesis/generation/coverage.py,sha256=
|
89
|
-
schemathesis/generation/meta.py,sha256=
|
88
|
+
schemathesis/generation/coverage.py,sha256=UUtY9OfL8TnSslWCrh17njN6Ik99u_qKxfUqcNNdHKw,56166
|
89
|
+
schemathesis/generation/meta.py,sha256=yYR7EB1f5n7RrzWHZ6YATepurnnc_hEe7HnztRbaaA0,2699
|
90
90
|
schemathesis/generation/metrics.py,sha256=cZU5HdeAMcLFEDnTbNE56NuNq4P0N4ew-g1NEz5-kt4,2836
|
91
91
|
schemathesis/generation/modes.py,sha256=Q1fhjWr3zxabU5qdtLvKfpMFZJAwlW9pnxgenjeXTyU,481
|
92
92
|
schemathesis/generation/overrides.py,sha256=OBWqDQPreiliaf2M-oyXppVKHoJkCRzxtwSJx1b6AFw,3759
|
93
93
|
schemathesis/generation/hypothesis/__init__.py,sha256=jK3G4i0SdcyhqwPQg91RH_yg437lSY-smeIQ-wZLWPc,1959
|
94
|
-
schemathesis/generation/hypothesis/builder.py,sha256=
|
94
|
+
schemathesis/generation/hypothesis/builder.py,sha256=zOUIp1n6kKkh_M7HiwtCNzDk3wtepwBEjmT2qt6PenM,36078
|
95
95
|
schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
|
96
96
|
schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
|
97
97
|
schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
|
@@ -116,14 +116,12 @@ schemathesis/python/asgi.py,sha256=5PyvuTBaivvyPUEi3pwJni91K1kX5Zc0u9c6c1D8a1Q,2
|
|
116
116
|
schemathesis/python/wsgi.py,sha256=uShAgo_NChbfYaV1117e6UHp0MTg7jaR0Sy_to3Jmf8,219
|
117
117
|
schemathesis/specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
118
118
|
schemathesis/specs/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
119
|
-
schemathesis/specs/graphql/_cache.py,sha256=mlOtzEvdE9gZ1posP7OqHUYlaZyiygL5U_Xda7wphFc,988
|
120
119
|
schemathesis/specs/graphql/nodes.py,sha256=bE3G1kNmqJ8OV4igBvIK-UORrkQA6Nofduf87O3TD9I,541
|
121
120
|
schemathesis/specs/graphql/scalars.py,sha256=6lew8mnwhrtg23leiEbG43mLGPLlRln8mClCY94XpDA,2680
|
122
|
-
schemathesis/specs/graphql/schemas.py,sha256=
|
121
|
+
schemathesis/specs/graphql/schemas.py,sha256=B6FULWIIWoN_gx9OeQOd-6qWsz9yVg5h1XlxFe06XPM,14097
|
123
122
|
schemathesis/specs/graphql/validation.py,sha256=-W1Noc1MQmTb4RX-gNXMeU2qkgso4mzVfHxtdLkCPKM,1422
|
124
123
|
schemathesis/specs/openapi/__init__.py,sha256=C5HOsfuDJGq_3mv8CRBvRvb0Diy1p0BFdqyEXMS-loE,238
|
125
|
-
schemathesis/specs/openapi/
|
126
|
-
schemathesis/specs/openapi/_hypothesis.py,sha256=shKwQPWupBSZgZ1m35HjSJi0HX0Yv5wqjdkYWA_EE7U,22399
|
124
|
+
schemathesis/specs/openapi/_hypothesis.py,sha256=-DE9jxLcEYn5S5wYTQmIC8Lr8UgyXZ8zIhPaB7qRakc,21308
|
127
125
|
schemathesis/specs/openapi/checks.py,sha256=1_YIcGqZ2xSN_1Ob_CIt_HfARSodMCCYNX4SQXx03-c,30150
|
128
126
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
129
127
|
schemathesis/specs/openapi/converter.py,sha256=LkpCCAxZzET4Qa_3YStSNuhGlsm5G6TVwpxYu6lPO4g,4169
|
@@ -131,10 +129,10 @@ schemathesis/specs/openapi/definitions.py,sha256=8htclglV3fW6JPBqs59lgM4LnA25Mm9
|
|
131
129
|
schemathesis/specs/openapi/examples.py,sha256=JsAy8Dexw36SUAO5MBB44ji3zEK9Y_JU5d2LRshjEI4,22023
|
132
130
|
schemathesis/specs/openapi/formats.py,sha256=8AIS7Uey-Z1wm1WYRqnsVqHwG9d316PdqfKLqwUs7us,3516
|
133
131
|
schemathesis/specs/openapi/media_types.py,sha256=F5M6TKl0s6Z5X8mZpPsWDEdPBvxclKRcUOc41eEwKbo,2472
|
134
|
-
schemathesis/specs/openapi/parameters.py,sha256=
|
132
|
+
schemathesis/specs/openapi/parameters.py,sha256=XpuZ2sex2aYUzKDK17GXVNWFBmvamuyVtloQ1oGQhLk,14468
|
135
133
|
schemathesis/specs/openapi/patterns.py,sha256=GqPZEXMRdWENQxanWjBOalIZ2MQUjuxk21kmdiI703E,18027
|
136
134
|
schemathesis/specs/openapi/references.py,sha256=40YcDExPLR2B8EOwt-Csw-5MYFi2xj_DXf91J0Pc9d4,8855
|
137
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
135
|
+
schemathesis/specs/openapi/schemas.py,sha256=71fdSVpC3_EDjIMMuQdZV7T32IzhdEHPEVTCZkyE3Co,50301
|
138
136
|
schemathesis/specs/openapi/security.py,sha256=6UWYMhL-dPtkTineqqBFNKca1i4EuoTduw-EOLeE0aQ,7149
|
139
137
|
schemathesis/specs/openapi/serialization.py,sha256=VdDLmeHqxlWM4cxQQcCkvrU6XurivolwEEaT13ohelA,11972
|
140
138
|
schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
|
@@ -148,7 +146,7 @@ schemathesis/specs/openapi/negative/__init__.py,sha256=1sajF22SrE4pUK7-C6PiseZ9P
|
|
148
146
|
schemathesis/specs/openapi/negative/mutations.py,sha256=xDSUVnGWjuuIcvmW_mJGChf-G-nXst-JBX1okQAzon4,19865
|
149
147
|
schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
|
150
148
|
schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
|
151
|
-
schemathesis/specs/openapi/stateful/__init__.py,sha256=
|
149
|
+
schemathesis/specs/openapi/stateful/__init__.py,sha256=DaV7Uuo1GTgZF1JjjoQzbzZw5HQhL6pTByiWqIlWwy4,15996
|
152
150
|
schemathesis/specs/openapi/stateful/control.py,sha256=QaXLSbwQWtai5lxvvVtQV3BLJ8n5ePqSKB00XFxp-MA,3695
|
153
151
|
schemathesis/specs/openapi/stateful/links.py,sha256=h5q40jUbcIk5DS_Tih1cvFJxS_QxxG0_9ZQnTs1A_zo,8806
|
154
152
|
schemathesis/transport/__init__.py,sha256=6yg_RfV_9L0cpA6qpbH-SL9_3ggtHQji9CZrpIkbA6s,5321
|
@@ -157,8 +155,8 @@ schemathesis/transport/prepare.py,sha256=erYXRaxpQokIDzaIuvt_csHcw72iHfCyNq8VNEz
|
|
157
155
|
schemathesis/transport/requests.py,sha256=46aplzhSmBupegPNMawma-iJWCegWkEd6mzdWLTpgM4,10742
|
158
156
|
schemathesis/transport/serialization.py,sha256=igUXKZ_VJ9gV7P0TUc5PDQBJXl_s0kK9T3ljGWWvo6E,10339
|
159
157
|
schemathesis/transport/wsgi.py,sha256=KoAfvu6RJtzyj24VGB8e-Iaa9smpgXJ3VsM8EgAz2tc,6152
|
160
|
-
schemathesis-4.0.
|
161
|
-
schemathesis-4.0.
|
162
|
-
schemathesis-4.0.
|
163
|
-
schemathesis-4.0.
|
164
|
-
schemathesis-4.0.
|
158
|
+
schemathesis-4.0.26.dist-info/METADATA,sha256=-G-Cvrj712t6wjZhCS-FOzJ_57yoPpQMM2scYjSAnI0,8472
|
159
|
+
schemathesis-4.0.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
160
|
+
schemathesis-4.0.26.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
|
161
|
+
schemathesis-4.0.26.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
|
162
|
+
schemathesis-4.0.26.dist-info/RECORD,,
|
@@ -1,35 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass
|
4
|
-
from typing import TYPE_CHECKING
|
5
|
-
|
6
|
-
if TYPE_CHECKING:
|
7
|
-
from ...schemas import APIOperation, APIOperationMap
|
8
|
-
|
9
|
-
|
10
|
-
@dataclass
|
11
|
-
class OperationCache:
|
12
|
-
_maps: dict[str, APIOperationMap]
|
13
|
-
_operations: dict[str, APIOperation]
|
14
|
-
|
15
|
-
__slots__ = ("_maps", "_operations")
|
16
|
-
|
17
|
-
def __init__(
|
18
|
-
self,
|
19
|
-
_maps: dict[str, APIOperationMap] | None = None,
|
20
|
-
_operations: dict[str, APIOperation] | None = None,
|
21
|
-
) -> None:
|
22
|
-
self._maps = _maps or {}
|
23
|
-
self._operations = _operations or {}
|
24
|
-
|
25
|
-
def get_map(self, key: str) -> APIOperationMap | None:
|
26
|
-
return self._maps.get(key)
|
27
|
-
|
28
|
-
def insert_map(self, key: str, value: APIOperationMap) -> None:
|
29
|
-
self._maps[key] = value
|
30
|
-
|
31
|
-
def get_operation(self, key: str) -> APIOperation | None:
|
32
|
-
return self._operations.get(key)
|
33
|
-
|
34
|
-
def insert_operation(self, key: str, value: APIOperation) -> None:
|
35
|
-
self._operations[key] = value
|
@@ -1,122 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass, field
|
4
|
-
from typing import TYPE_CHECKING, Any, Tuple
|
5
|
-
|
6
|
-
if TYPE_CHECKING:
|
7
|
-
from ...schemas import APIOperation, APIOperationMap
|
8
|
-
|
9
|
-
|
10
|
-
@dataclass
|
11
|
-
class OperationCacheEntry:
|
12
|
-
path: str
|
13
|
-
method: str
|
14
|
-
# The resolution scope of the operation
|
15
|
-
scope: str
|
16
|
-
# Parent path item
|
17
|
-
path_item: dict[str, Any]
|
18
|
-
# Unresolved operation definition
|
19
|
-
operation: dict[str, Any]
|
20
|
-
__slots__ = ("path", "method", "scope", "path_item", "operation")
|
21
|
-
|
22
|
-
|
23
|
-
# During traversal, we need to keep track of the scope, path, and method
|
24
|
-
TraversalKey = Tuple[str, str, str]
|
25
|
-
OperationId = str
|
26
|
-
Reference = str
|
27
|
-
|
28
|
-
|
29
|
-
@dataclass
|
30
|
-
class OperationCache:
|
31
|
-
"""Cache for Open API operations.
|
32
|
-
|
33
|
-
This cache contains multiple levels to avoid unnecessary parsing of the schema.
|
34
|
-
"""
|
35
|
-
|
36
|
-
# Cache to avoid schema traversal on every access
|
37
|
-
_id_to_definition: dict[OperationId, OperationCacheEntry] = field(default_factory=dict)
|
38
|
-
# Map map between 1st & 2nd level cache keys
|
39
|
-
# Even though 1st level keys could be directly mapped to Python objects in memory, we need to keep them separate
|
40
|
-
# to ensure a single owner of the operation instance.
|
41
|
-
_id_to_operation: dict[OperationId, int] = field(default_factory=dict)
|
42
|
-
_traversal_key_to_operation: dict[TraversalKey, int] = field(default_factory=dict)
|
43
|
-
_reference_to_operation: dict[Reference, int] = field(default_factory=dict)
|
44
|
-
# The actual operations
|
45
|
-
_operations: list[APIOperation] = field(default_factory=list)
|
46
|
-
# Cache for operation maps
|
47
|
-
_maps: dict[str, APIOperationMap] = field(default_factory=dict)
|
48
|
-
|
49
|
-
@property
|
50
|
-
def known_operation_ids(self) -> list[str]:
|
51
|
-
return list(self._id_to_definition)
|
52
|
-
|
53
|
-
@property
|
54
|
-
def has_ids_to_definitions(self) -> bool:
|
55
|
-
return bool(self._id_to_definition)
|
56
|
-
|
57
|
-
def _append_operation(self, operation: APIOperation) -> int:
|
58
|
-
idx = len(self._operations)
|
59
|
-
self._operations.append(operation)
|
60
|
-
return idx
|
61
|
-
|
62
|
-
def insert_definition_by_id(
|
63
|
-
self,
|
64
|
-
operation_id: str,
|
65
|
-
path: str,
|
66
|
-
method: str,
|
67
|
-
scope: str,
|
68
|
-
path_item: dict[str, Any],
|
69
|
-
operation: dict[str, Any],
|
70
|
-
) -> None:
|
71
|
-
"""Insert a new operation definition into cache."""
|
72
|
-
self._id_to_definition[operation_id] = OperationCacheEntry(
|
73
|
-
path=path, method=method, scope=scope, path_item=path_item, operation=operation
|
74
|
-
)
|
75
|
-
|
76
|
-
def get_definition_by_id(self, operation_id: str) -> OperationCacheEntry:
|
77
|
-
"""Get an operation definition by its ID."""
|
78
|
-
# TODO: Avoid KeyError in the future
|
79
|
-
return self._id_to_definition[operation_id]
|
80
|
-
|
81
|
-
def insert_operation(
|
82
|
-
self,
|
83
|
-
operation: APIOperation,
|
84
|
-
*,
|
85
|
-
traversal_key: TraversalKey,
|
86
|
-
operation_id: str | None = None,
|
87
|
-
reference: str | None = None,
|
88
|
-
) -> None:
|
89
|
-
"""Insert a new operation into cache by one or multiple keys."""
|
90
|
-
idx = self._append_operation(operation)
|
91
|
-
self._traversal_key_to_operation[traversal_key] = idx
|
92
|
-
if operation_id is not None:
|
93
|
-
self._id_to_operation[operation_id] = idx
|
94
|
-
if reference is not None:
|
95
|
-
self._reference_to_operation[reference] = idx
|
96
|
-
|
97
|
-
def get_operation_by_id(self, operation_id: str) -> APIOperation | None:
|
98
|
-
"""Get an operation by its ID."""
|
99
|
-
idx = self._id_to_operation.get(operation_id)
|
100
|
-
if idx is not None:
|
101
|
-
return self._operations[idx]
|
102
|
-
return None
|
103
|
-
|
104
|
-
def get_operation_by_reference(self, reference: str) -> APIOperation | None:
|
105
|
-
"""Get an operation by its reference."""
|
106
|
-
idx = self._reference_to_operation.get(reference)
|
107
|
-
if idx is not None:
|
108
|
-
return self._operations[idx]
|
109
|
-
return None
|
110
|
-
|
111
|
-
def get_operation_by_traversal_key(self, key: TraversalKey) -> APIOperation | None:
|
112
|
-
"""Get an operation by its traverse key."""
|
113
|
-
idx = self._traversal_key_to_operation.get(key)
|
114
|
-
if idx is not None:
|
115
|
-
return self._operations[idx]
|
116
|
-
return None
|
117
|
-
|
118
|
-
def get_map(self, key: str) -> APIOperationMap | None:
|
119
|
-
return self._maps.get(key)
|
120
|
-
|
121
|
-
def insert_map(self, key: str, value: APIOperationMap) -> None:
|
122
|
-
self._maps[key] = value
|
File without changes
|
File without changes
|
File without changes
|