schemathesis 3.32.0__py3-none-any.whl → 3.32.1__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/filters.py +104 -5
- schemathesis/lazy.py +10 -3
- schemathesis/runner/events.py +22 -1
- schemathesis/runner/serialization.py +70 -1
- schemathesis/schemas.py +17 -30
- schemathesis/service/serialization.py +3 -124
- schemathesis/specs/openapi/loaders.py +12 -10
- schemathesis/specs/openapi/schemas.py +29 -23
- schemathesis/stateful/events.py +48 -2
- {schemathesis-3.32.0.dist-info → schemathesis-3.32.1.dist-info}/METADATA +1 -1
- {schemathesis-3.32.0.dist-info → schemathesis-3.32.1.dist-info}/RECORD +14 -15
- schemathesis/specs/openapi/filters.py +0 -50
- {schemathesis-3.32.0.dist-info → schemathesis-3.32.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.32.0.dist-info → schemathesis-3.32.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.32.0.dist-info → schemathesis-3.32.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/filters.py
CHANGED
|
@@ -8,6 +8,8 @@ from functools import partial
|
|
|
8
8
|
from types import SimpleNamespace
|
|
9
9
|
from typing import TYPE_CHECKING, Callable, List, Protocol, Union
|
|
10
10
|
|
|
11
|
+
from .types import NotSet, Filter as FilterType
|
|
12
|
+
|
|
11
13
|
from .exceptions import UsageError
|
|
12
14
|
|
|
13
15
|
if TYPE_CHECKING:
|
|
@@ -58,7 +60,12 @@ class Matcher:
|
|
|
58
60
|
def for_regex(cls, attribute: str, regex: RegexValue) -> Matcher:
|
|
59
61
|
"""Matcher that checks whether the specified attribute has the provided regex."""
|
|
60
62
|
if isinstance(regex, str):
|
|
61
|
-
|
|
63
|
+
flags: re.RegexFlag | int
|
|
64
|
+
if attribute == "method":
|
|
65
|
+
flags = re.IGNORECASE
|
|
66
|
+
else:
|
|
67
|
+
flags = 0
|
|
68
|
+
regex = re.compile(regex, flags=flags)
|
|
62
69
|
func = partial(by_regex, attribute=attribute, regex=regex)
|
|
63
70
|
label = f"{attribute}_regex={repr(regex)}"
|
|
64
71
|
return cls(func, label=label, _hash=hash(label))
|
|
@@ -71,6 +78,8 @@ class Matcher:
|
|
|
71
78
|
def get_operation_attribute(operation: APIOperation, attribute: str) -> str | list[str] | None:
|
|
72
79
|
if attribute == "tag":
|
|
73
80
|
return operation.tags
|
|
81
|
+
if attribute == "operation_id":
|
|
82
|
+
return operation.definition.raw.get("operationId")
|
|
74
83
|
# Just uppercase `method`
|
|
75
84
|
value = getattr(operation, attribute)
|
|
76
85
|
if attribute == "method":
|
|
@@ -101,8 +110,8 @@ def by_regex(ctx: HasAPIOperation, attribute: str, regex: re.Pattern) -> bool:
|
|
|
101
110
|
if value is None:
|
|
102
111
|
return False
|
|
103
112
|
if isinstance(value, list):
|
|
104
|
-
return any(bool(regex.
|
|
105
|
-
return bool(regex.
|
|
113
|
+
return any(bool(regex.search(entry)) for entry in value)
|
|
114
|
+
return bool(regex.search(value))
|
|
106
115
|
|
|
107
116
|
|
|
108
117
|
@dataclass(repr=False, frozen=True)
|
|
@@ -111,6 +120,8 @@ class Filter:
|
|
|
111
120
|
|
|
112
121
|
matchers: tuple[Matcher, ...]
|
|
113
122
|
|
|
123
|
+
__slots__ = ("matchers",)
|
|
124
|
+
|
|
114
125
|
def __repr__(self) -> str:
|
|
115
126
|
inner = " && ".join(matcher.label for matcher in self.matchers)
|
|
116
127
|
return f"<{self.__class__.__name__}: [{inner}]>"
|
|
@@ -127,8 +138,14 @@ class Filter:
|
|
|
127
138
|
class FilterSet:
|
|
128
139
|
"""Combines multiple filters to apply inclusion and exclusion rules on API operations."""
|
|
129
140
|
|
|
130
|
-
_includes: set[Filter]
|
|
131
|
-
_excludes: set[Filter]
|
|
141
|
+
_includes: set[Filter]
|
|
142
|
+
_excludes: set[Filter]
|
|
143
|
+
|
|
144
|
+
__slots__ = ("_includes", "_excludes")
|
|
145
|
+
|
|
146
|
+
def __init__(self) -> None:
|
|
147
|
+
self._includes = set()
|
|
148
|
+
self._excludes = set()
|
|
132
149
|
|
|
133
150
|
def apply_to(self, operations: list[APIOperation]) -> list[APIOperation]:
|
|
134
151
|
"""Get a filtered list of the given operations that match the filters."""
|
|
@@ -168,6 +185,8 @@ class FilterSet:
|
|
|
168
185
|
path_regex: RegexValue | None = None,
|
|
169
186
|
tag: FilterValue | None = None,
|
|
170
187
|
tag_regex: RegexValue | None = None,
|
|
188
|
+
operation_id: FilterValue | None = None,
|
|
189
|
+
operation_id_regex: RegexValue | None = None,
|
|
171
190
|
) -> None:
|
|
172
191
|
"""Add a new INCLUDE filter."""
|
|
173
192
|
self._add_filter(
|
|
@@ -181,6 +200,8 @@ class FilterSet:
|
|
|
181
200
|
path_regex=path_regex,
|
|
182
201
|
tag=tag,
|
|
183
202
|
tag_regex=tag_regex,
|
|
203
|
+
operation_id=operation_id,
|
|
204
|
+
operation_id_regex=operation_id_regex,
|
|
184
205
|
)
|
|
185
206
|
|
|
186
207
|
def exclude(
|
|
@@ -195,6 +216,8 @@ class FilterSet:
|
|
|
195
216
|
path_regex: RegexValue | None = None,
|
|
196
217
|
tag: FilterValue | None = None,
|
|
197
218
|
tag_regex: RegexValue | None = None,
|
|
219
|
+
operation_id: FilterValue | None = None,
|
|
220
|
+
operation_id_regex: RegexValue | None = None,
|
|
198
221
|
) -> None:
|
|
199
222
|
"""Add a new EXCLUDE filter."""
|
|
200
223
|
self._add_filter(
|
|
@@ -208,6 +231,8 @@ class FilterSet:
|
|
|
208
231
|
path_regex=path_regex,
|
|
209
232
|
tag=tag,
|
|
210
233
|
tag_regex=tag_regex,
|
|
234
|
+
operation_id=operation_id,
|
|
235
|
+
operation_id_regex=operation_id_regex,
|
|
211
236
|
)
|
|
212
237
|
|
|
213
238
|
def _add_filter(
|
|
@@ -223,6 +248,8 @@ class FilterSet:
|
|
|
223
248
|
path_regex: RegexValue | None = None,
|
|
224
249
|
tag: FilterValue | None = None,
|
|
225
250
|
tag_regex: RegexValue | None = None,
|
|
251
|
+
operation_id: FilterValue | None = None,
|
|
252
|
+
operation_id_regex: RegexValue | None = None,
|
|
226
253
|
) -> None:
|
|
227
254
|
matchers = []
|
|
228
255
|
if func is not None:
|
|
@@ -232,6 +259,7 @@ class FilterSet:
|
|
|
232
259
|
("method", method, method_regex),
|
|
233
260
|
("path", path, path_regex),
|
|
234
261
|
("tag", tag, tag_regex),
|
|
262
|
+
("operation_id", operation_id, operation_id_regex),
|
|
235
263
|
):
|
|
236
264
|
if expected is not None and regex is not None:
|
|
237
265
|
# To match anything the regex should match the expected value, hence passing them together is useless
|
|
@@ -295,3 +323,74 @@ def attach_filter_chain(
|
|
|
295
323
|
proxy.__name__ = attribute
|
|
296
324
|
|
|
297
325
|
setattr(target, attribute, proxy)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def filter_set_from_components(
|
|
329
|
+
*,
|
|
330
|
+
include: bool,
|
|
331
|
+
method: FilterType | None = None,
|
|
332
|
+
endpoint: FilterType | None = None,
|
|
333
|
+
tag: FilterType | None = None,
|
|
334
|
+
operation_id: FilterType | None = None,
|
|
335
|
+
skip_deprecated_operations: bool | None | NotSet = None,
|
|
336
|
+
parent: FilterSet | None = None,
|
|
337
|
+
) -> FilterSet:
|
|
338
|
+
def _is_defined(x: FilterType | None) -> bool:
|
|
339
|
+
return x is not None and not isinstance(x, NotSet)
|
|
340
|
+
|
|
341
|
+
def _is_deprecated(ctx: HasAPIOperation) -> bool:
|
|
342
|
+
return ctx.operation.definition.raw.get("deprecated") is True
|
|
343
|
+
|
|
344
|
+
def _prepare_filter(filter_: FilterType | None) -> RegexValue | None:
|
|
345
|
+
if filter_ is None or isinstance(filter_, NotSet):
|
|
346
|
+
return None
|
|
347
|
+
if isinstance(filter_, str):
|
|
348
|
+
return filter_
|
|
349
|
+
return "|".join(f"({f})" for f in filter_)
|
|
350
|
+
|
|
351
|
+
new = FilterSet()
|
|
352
|
+
|
|
353
|
+
if _is_defined(method) or _is_defined(endpoint) or _is_defined(tag) or _is_defined(operation_id):
|
|
354
|
+
new._add_filter(
|
|
355
|
+
include,
|
|
356
|
+
method_regex=_prepare_filter(method),
|
|
357
|
+
path_regex=_prepare_filter(endpoint),
|
|
358
|
+
tag_regex=_prepare_filter(tag),
|
|
359
|
+
operation_id_regex=_prepare_filter(operation_id),
|
|
360
|
+
)
|
|
361
|
+
if skip_deprecated_operations is True and not any(
|
|
362
|
+
matcher.label == _is_deprecated.__name__ for exclude_ in new._excludes for matcher in exclude_.matchers
|
|
363
|
+
):
|
|
364
|
+
new.exclude(func=_is_deprecated)
|
|
365
|
+
# Merge with the parent filter set
|
|
366
|
+
if parent is not None:
|
|
367
|
+
for include_ in parent._includes:
|
|
368
|
+
matchers = include_.matchers
|
|
369
|
+
ids = []
|
|
370
|
+
for idx, matcher in enumerate(matchers):
|
|
371
|
+
label = matcher.label
|
|
372
|
+
if (
|
|
373
|
+
(not isinstance(method, NotSet) and label.startswith("method_regex="))
|
|
374
|
+
or (not isinstance(endpoint, NotSet) and label.startswith("path_regex="))
|
|
375
|
+
or (not isinstance(tag, NotSet) and matcher.label.startswith("tag_regex="))
|
|
376
|
+
or (not isinstance(operation_id, NotSet) and matcher.label.startswith("operation_id_regex="))
|
|
377
|
+
):
|
|
378
|
+
ids.append(idx)
|
|
379
|
+
if ids:
|
|
380
|
+
matchers = tuple(matcher for idx, matcher in enumerate(matchers) if idx not in ids)
|
|
381
|
+
if matchers:
|
|
382
|
+
if new._includes:
|
|
383
|
+
existing = new._includes.pop()
|
|
384
|
+
matchers = existing.matchers + matchers
|
|
385
|
+
new._includes.add(Filter(matchers=matchers))
|
|
386
|
+
for exclude_ in parent._excludes:
|
|
387
|
+
matchers = exclude_.matchers
|
|
388
|
+
ids = []
|
|
389
|
+
for idx, matcher in enumerate(exclude_.matchers):
|
|
390
|
+
if skip_deprecated_operations is False and matcher.label == _is_deprecated.__name__:
|
|
391
|
+
ids.append(idx)
|
|
392
|
+
if ids:
|
|
393
|
+
matchers = tuple(matcher for idx, matcher in enumerate(matchers) if idx not in ids)
|
|
394
|
+
if matchers:
|
|
395
|
+
new._excludes.add(exclude_)
|
|
396
|
+
return new
|
schemathesis/lazy.py
CHANGED
|
@@ -19,6 +19,7 @@ from .auths import AuthStorage
|
|
|
19
19
|
from .code_samples import CodeSampleStyle
|
|
20
20
|
from .constants import FLAKY_FAILURE_MESSAGE, NOT_SET
|
|
21
21
|
from .exceptions import CheckFailed, OperationSchemaError, SkipTest, get_grouped_exception
|
|
22
|
+
from .filters import filter_set_from_components
|
|
22
23
|
from .generation import DataGenerationMethodInput, GenerationConfig
|
|
23
24
|
from .hooks import HookDispatcher, HookScope
|
|
24
25
|
from .internal.output import OutputConfig
|
|
@@ -341,18 +342,24 @@ def get_schema(
|
|
|
341
342
|
schema = request.getfixturevalue(name)
|
|
342
343
|
if not isinstance(schema, BaseSchema):
|
|
343
344
|
raise ValueError(f"The given schema must be an instance of BaseSchema, got: {type(schema)}")
|
|
344
|
-
|
|
345
|
-
|
|
345
|
+
|
|
346
|
+
filter_set = filter_set_from_components(
|
|
347
|
+
include=True,
|
|
346
348
|
method=method,
|
|
347
349
|
endpoint=endpoint,
|
|
348
350
|
tag=tag,
|
|
349
351
|
operation_id=operation_id,
|
|
352
|
+
skip_deprecated_operations=skip_deprecated_operations,
|
|
353
|
+
parent=schema.filter_set,
|
|
354
|
+
)
|
|
355
|
+
return schema.clone(
|
|
356
|
+
base_url=base_url,
|
|
357
|
+
filter_set=filter_set,
|
|
350
358
|
app=app,
|
|
351
359
|
test_function=test_function,
|
|
352
360
|
hooks=schema.hooks.merge(hooks),
|
|
353
361
|
auth=auth,
|
|
354
362
|
validate_schema=validate_schema,
|
|
355
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
356
363
|
data_generation_methods=data_generation_methods,
|
|
357
364
|
generation_config=generation_config,
|
|
358
365
|
output_config=output_config,
|
schemathesis/runner/events.py
CHANGED
|
@@ -9,7 +9,8 @@ from typing import TYPE_CHECKING, Any
|
|
|
9
9
|
from ..exceptions import RuntimeErrorType, SchemaError, SchemaErrorType, format_exception
|
|
10
10
|
from ..generation import DataGenerationMethod
|
|
11
11
|
from ..internal.datetime import current_datetime
|
|
12
|
-
from ..internal.result import Result
|
|
12
|
+
from ..internal.result import Err, Ok, Result
|
|
13
|
+
from ..service.models import AnalysisSuccess
|
|
13
14
|
from .serialization import SerializedError, SerializedTestResult
|
|
14
15
|
|
|
15
16
|
if TYPE_CHECKING:
|
|
@@ -105,6 +106,23 @@ class BeforeAnalysis(ExecutionEvent):
|
|
|
105
106
|
class AfterAnalysis(ExecutionEvent):
|
|
106
107
|
analysis: Result[AnalysisResult, Exception] | None
|
|
107
108
|
|
|
109
|
+
def _serialize(self) -> dict[str, Any]:
|
|
110
|
+
data = {}
|
|
111
|
+
if isinstance(self.analysis, Ok):
|
|
112
|
+
result = self.analysis.ok()
|
|
113
|
+
if isinstance(result, AnalysisSuccess):
|
|
114
|
+
data["analysis_id"] = result.id
|
|
115
|
+
else:
|
|
116
|
+
data["error"] = result.message
|
|
117
|
+
elif isinstance(self.analysis, Err):
|
|
118
|
+
data["error"] = format_exception(self.analysis.err())
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
def asdict(self, **kwargs: Any) -> dict[str, Any]:
|
|
122
|
+
data = self._serialize()
|
|
123
|
+
data["event_type"] = self.__class__.__name__
|
|
124
|
+
return data
|
|
125
|
+
|
|
108
126
|
|
|
109
127
|
class CurrentOperationMixin:
|
|
110
128
|
method: str
|
|
@@ -296,6 +314,9 @@ class StatefulEvent(ExecutionEvent):
|
|
|
296
314
|
|
|
297
315
|
__slots__ = ("data",)
|
|
298
316
|
|
|
317
|
+
def asdict(self, **kwargs: Any) -> dict[str, Any]:
|
|
318
|
+
return {"data": self.data.asdict(**kwargs), "event_type": self.__class__.__name__}
|
|
319
|
+
|
|
299
320
|
|
|
300
321
|
@dataclass
|
|
301
322
|
class AfterStatefulExecution(ExecutionEvent):
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
import logging
|
|
9
9
|
import re
|
|
10
10
|
import textwrap
|
|
11
|
-
from dataclasses import dataclass, field
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
|
12
12
|
from typing import TYPE_CHECKING, Any, cast
|
|
13
13
|
|
|
14
14
|
from ..code_samples import get_excluded_headers
|
|
@@ -453,3 +453,72 @@ def deduplicate_failures(checks: list[SerializedCheck]) -> list[SerializedCheck]
|
|
|
453
453
|
unique_checks.append(check)
|
|
454
454
|
seen.add(key)
|
|
455
455
|
return unique_checks
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _serialize_case(case: SerializedCase) -> dict[str, Any]:
|
|
459
|
+
return {
|
|
460
|
+
"id": case.id,
|
|
461
|
+
"generation_time": case.generation_time,
|
|
462
|
+
"verbose_name": case.verbose_name,
|
|
463
|
+
"path_template": case.path_template,
|
|
464
|
+
"path_parameters": stringify_path_parameters(case.path_parameters),
|
|
465
|
+
"query": prepare_query(case.query),
|
|
466
|
+
"cookies": case.cookies,
|
|
467
|
+
"media_type": case.media_type,
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _serialize_response(response: Response) -> dict[str, Any]:
|
|
472
|
+
return {
|
|
473
|
+
"status_code": response.status_code,
|
|
474
|
+
"headers": response.headers,
|
|
475
|
+
"body": response.body,
|
|
476
|
+
"encoding": response.encoding,
|
|
477
|
+
"elapsed": response.elapsed,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _serialize_check(check: SerializedCheck) -> dict[str, Any]:
|
|
482
|
+
return {
|
|
483
|
+
"name": check.name,
|
|
484
|
+
"value": check.value,
|
|
485
|
+
"request": {
|
|
486
|
+
"method": check.request.method,
|
|
487
|
+
"uri": check.request.uri,
|
|
488
|
+
"body": check.request.body,
|
|
489
|
+
"headers": check.request.headers,
|
|
490
|
+
},
|
|
491
|
+
"response": _serialize_response(check.response) if check.response is not None else None,
|
|
492
|
+
"example": _serialize_case(check.example),
|
|
493
|
+
"message": check.message,
|
|
494
|
+
"context": asdict(check.context) if check.context is not None else None, # type: ignore
|
|
495
|
+
"history": [
|
|
496
|
+
{"case": _serialize_case(entry.case), "response": _serialize_response(entry.response)}
|
|
497
|
+
for entry in check.history
|
|
498
|
+
],
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def stringify_path_parameters(path_parameters: dict[str, Any] | None) -> dict[str, str]:
|
|
503
|
+
"""Cast all path parameter values to strings.
|
|
504
|
+
|
|
505
|
+
Path parameter values may be of arbitrary type, but to display them properly they should be casted to strings.
|
|
506
|
+
"""
|
|
507
|
+
return {key: str(value) for key, value in (path_parameters or {}).items()}
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def prepare_query(query: dict[str, Any] | None) -> dict[str, list[str]]:
|
|
511
|
+
"""Convert all query values to list of strings.
|
|
512
|
+
|
|
513
|
+
Query parameters may be generated in different shapes, including integers, strings, list of strings, etc.
|
|
514
|
+
It can also be an object, if the schema contains an object, but `style` and `explode` combo is not applicable.
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
def to_list_of_strings(value: Any) -> list[str]:
|
|
518
|
+
if isinstance(value, list):
|
|
519
|
+
return list(map(str, value))
|
|
520
|
+
if isinstance(value, str):
|
|
521
|
+
return [value]
|
|
522
|
+
return [str(value)]
|
|
523
|
+
|
|
524
|
+
return {key: to_list_of_strings(value) for key, value in (query or {}).items()}
|
schemathesis/schemas.py
CHANGED
|
@@ -37,6 +37,7 @@ from .auths import AuthStorage
|
|
|
37
37
|
from .code_samples import CodeSampleStyle
|
|
38
38
|
from .constants import NOT_SET
|
|
39
39
|
from .exceptions import OperationSchemaError, UsageError
|
|
40
|
+
from .filters import FilterSet, filter_set_from_components
|
|
40
41
|
from .generation import (
|
|
41
42
|
DEFAULT_DATA_GENERATION_METHODS,
|
|
42
43
|
DataGenerationMethod,
|
|
@@ -81,16 +82,12 @@ class BaseSchema(Mapping):
|
|
|
81
82
|
transport: Transport
|
|
82
83
|
location: str | None = None
|
|
83
84
|
base_url: str | None = None
|
|
84
|
-
|
|
85
|
-
endpoint: Filter | None = None
|
|
86
|
-
tag: Filter | None = None
|
|
87
|
-
operation_id: Filter | None = None
|
|
85
|
+
filter_set: FilterSet = field(default_factory=FilterSet)
|
|
88
86
|
app: Any = None
|
|
89
87
|
hooks: HookDispatcher = field(default_factory=lambda: HookDispatcher(scope=HookScope.SCHEMA))
|
|
90
88
|
auth: AuthStorage = field(default_factory=AuthStorage)
|
|
91
89
|
test_function: GenericTest | None = None
|
|
92
90
|
validate_schema: bool = True
|
|
93
|
-
skip_deprecated_operations: bool = False
|
|
94
91
|
data_generation_methods: list[DataGenerationMethod] = field(
|
|
95
92
|
default_factory=lambda: list(DEFAULT_DATA_GENERATION_METHODS)
|
|
96
93
|
)
|
|
@@ -242,6 +239,16 @@ class BaseSchema(Mapping):
|
|
|
242
239
|
CodeSampleStyle.from_str(code_sample_style) if isinstance(code_sample_style, str) else code_sample_style
|
|
243
240
|
)
|
|
244
241
|
|
|
242
|
+
filter_set = filter_set_from_components(
|
|
243
|
+
include=True,
|
|
244
|
+
method=method,
|
|
245
|
+
endpoint=endpoint,
|
|
246
|
+
tag=tag,
|
|
247
|
+
operation_id=operation_id,
|
|
248
|
+
skip_deprecated_operations=skip_deprecated_operations,
|
|
249
|
+
parent=self.filter_set,
|
|
250
|
+
)
|
|
251
|
+
|
|
245
252
|
def wrapper(func: GenericTest) -> GenericTest:
|
|
246
253
|
if hasattr(func, PARAMETRIZE_MARKER):
|
|
247
254
|
|
|
@@ -256,13 +263,9 @@ class BaseSchema(Mapping):
|
|
|
256
263
|
HookDispatcher.add_dispatcher(func)
|
|
257
264
|
cloned = self.clone(
|
|
258
265
|
test_function=func,
|
|
259
|
-
method=method,
|
|
260
|
-
endpoint=endpoint,
|
|
261
|
-
tag=tag,
|
|
262
|
-
operation_id=operation_id,
|
|
263
266
|
validate_schema=validate_schema,
|
|
264
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
265
267
|
data_generation_methods=data_generation_methods,
|
|
268
|
+
filter_set=filter_set,
|
|
266
269
|
code_sample_style=_code_sample_style, # type: ignore
|
|
267
270
|
)
|
|
268
271
|
setattr(func, PARAMETRIZE_MARKER, cloned)
|
|
@@ -279,38 +282,26 @@ class BaseSchema(Mapping):
|
|
|
279
282
|
*,
|
|
280
283
|
base_url: str | None | NotSet = NOT_SET,
|
|
281
284
|
test_function: GenericTest | None = None,
|
|
282
|
-
method: Filter | None = NOT_SET,
|
|
283
|
-
endpoint: Filter | None = NOT_SET,
|
|
284
|
-
tag: Filter | None = NOT_SET,
|
|
285
|
-
operation_id: Filter | None = NOT_SET,
|
|
286
285
|
app: Any = NOT_SET,
|
|
287
286
|
hooks: HookDispatcher | NotSet = NOT_SET,
|
|
288
287
|
auth: AuthStorage | NotSet = NOT_SET,
|
|
289
288
|
validate_schema: bool | NotSet = NOT_SET,
|
|
290
|
-
skip_deprecated_operations: bool | NotSet = NOT_SET,
|
|
291
289
|
data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
|
|
292
290
|
generation_config: GenerationConfig | NotSet = NOT_SET,
|
|
293
291
|
output_config: OutputConfig | NotSet = NOT_SET,
|
|
294
292
|
code_sample_style: CodeSampleStyle | NotSet = NOT_SET,
|
|
295
293
|
rate_limiter: Limiter | None = NOT_SET,
|
|
296
294
|
sanitize_output: bool | NotSet | None = NOT_SET,
|
|
295
|
+
filter_set: FilterSet | None = None,
|
|
297
296
|
) -> BaseSchema:
|
|
298
297
|
if base_url is NOT_SET:
|
|
299
298
|
base_url = self.base_url
|
|
300
|
-
if method is NOT_SET:
|
|
301
|
-
method = self.method
|
|
302
|
-
if endpoint is NOT_SET:
|
|
303
|
-
endpoint = self.endpoint
|
|
304
|
-
if tag is NOT_SET:
|
|
305
|
-
tag = self.tag
|
|
306
|
-
if operation_id is NOT_SET:
|
|
307
|
-
operation_id = self.operation_id
|
|
308
299
|
if app is NOT_SET:
|
|
309
300
|
app = self.app
|
|
310
301
|
if validate_schema is NOT_SET:
|
|
311
302
|
validate_schema = self.validate_schema
|
|
312
|
-
if
|
|
313
|
-
|
|
303
|
+
if filter_set is None:
|
|
304
|
+
filter_set = self.filter_set
|
|
314
305
|
if hooks is NOT_SET:
|
|
315
306
|
hooks = self.hooks
|
|
316
307
|
if auth is NOT_SET:
|
|
@@ -332,22 +323,18 @@ class BaseSchema(Mapping):
|
|
|
332
323
|
self.raw_schema,
|
|
333
324
|
location=self.location,
|
|
334
325
|
base_url=base_url, # type: ignore
|
|
335
|
-
method=method,
|
|
336
|
-
endpoint=endpoint,
|
|
337
|
-
tag=tag,
|
|
338
|
-
operation_id=operation_id,
|
|
339
326
|
app=app,
|
|
340
327
|
hooks=hooks, # type: ignore
|
|
341
328
|
auth=auth, # type: ignore
|
|
342
329
|
test_function=test_function,
|
|
343
330
|
validate_schema=validate_schema, # type: ignore
|
|
344
|
-
skip_deprecated_operations=skip_deprecated_operations, # type: ignore
|
|
345
331
|
data_generation_methods=data_generation_methods, # type: ignore
|
|
346
332
|
generation_config=generation_config, # type: ignore
|
|
347
333
|
output_config=output_config, # type: ignore
|
|
348
334
|
code_sample_style=code_sample_style, # type: ignore
|
|
349
335
|
rate_limiter=rate_limiter, # type: ignore
|
|
350
336
|
sanitize_output=sanitize_output, # type: ignore
|
|
337
|
+
filter_set=filter_set, # type: ignore
|
|
351
338
|
transport=self.transport,
|
|
352
339
|
)
|
|
353
340
|
|
|
@@ -3,14 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import asdict
|
|
4
4
|
from typing import Any, Callable, Dict, Optional, TypeVar, cast
|
|
5
5
|
|
|
6
|
-
from ..exceptions import format_exception
|
|
7
|
-
from ..internal.result import Err, Ok
|
|
8
6
|
from ..internal.transformation import merge_recursively
|
|
9
|
-
from ..models import Response
|
|
10
7
|
from ..runner import events
|
|
11
|
-
from ..runner.serialization import
|
|
8
|
+
from ..runner.serialization import _serialize_check
|
|
12
9
|
from ..stateful import events as stateful_events
|
|
13
|
-
from .models import AnalysisSuccess
|
|
14
10
|
|
|
15
11
|
S = TypeVar("S", bound=events.ExecutionEvent)
|
|
16
12
|
SerializeFunc = Callable[[S], Optional[Dict[str, Any]]]
|
|
@@ -38,17 +34,7 @@ def serialize_before_analysis(_: events.BeforeAnalysis) -> None:
|
|
|
38
34
|
|
|
39
35
|
|
|
40
36
|
def serialize_after_analysis(event: events.AfterAnalysis) -> dict[str, Any] | None:
|
|
41
|
-
|
|
42
|
-
analysis = event.analysis
|
|
43
|
-
if isinstance(analysis, Ok):
|
|
44
|
-
result = analysis.ok()
|
|
45
|
-
if isinstance(result, AnalysisSuccess):
|
|
46
|
-
data["analysis_id"] = result.id
|
|
47
|
-
else:
|
|
48
|
-
data["error"] = result.message
|
|
49
|
-
elif isinstance(analysis, Err):
|
|
50
|
-
data["error"] = format_exception(analysis.err())
|
|
51
|
-
return data
|
|
37
|
+
return event._serialize()
|
|
52
38
|
|
|
53
39
|
|
|
54
40
|
def serialize_before_execution(event: events.BeforeExecution) -> dict[str, Any] | None:
|
|
@@ -59,50 +45,6 @@ def serialize_before_execution(event: events.BeforeExecution) -> dict[str, Any]
|
|
|
59
45
|
}
|
|
60
46
|
|
|
61
47
|
|
|
62
|
-
def _serialize_case(case: SerializedCase) -> dict[str, Any]:
|
|
63
|
-
return {
|
|
64
|
-
"id": case.id,
|
|
65
|
-
"generation_time": case.generation_time,
|
|
66
|
-
"verbose_name": case.verbose_name,
|
|
67
|
-
"path_template": case.path_template,
|
|
68
|
-
"path_parameters": stringify_path_parameters(case.path_parameters),
|
|
69
|
-
"query": prepare_query(case.query),
|
|
70
|
-
"cookies": case.cookies,
|
|
71
|
-
"media_type": case.media_type,
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _serialize_response(response: Response) -> dict[str, Any]:
|
|
76
|
-
return {
|
|
77
|
-
"status_code": response.status_code,
|
|
78
|
-
"headers": response.headers,
|
|
79
|
-
"body": response.body,
|
|
80
|
-
"encoding": response.encoding,
|
|
81
|
-
"elapsed": response.elapsed,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _serialize_check(check: SerializedCheck) -> dict[str, Any]:
|
|
86
|
-
return {
|
|
87
|
-
"name": check.name,
|
|
88
|
-
"value": check.value,
|
|
89
|
-
"request": {
|
|
90
|
-
"method": check.request.method,
|
|
91
|
-
"uri": check.request.uri,
|
|
92
|
-
"body": check.request.body,
|
|
93
|
-
"headers": check.request.headers,
|
|
94
|
-
},
|
|
95
|
-
"response": _serialize_response(check.response) if check.response is not None else None,
|
|
96
|
-
"example": _serialize_case(check.example),
|
|
97
|
-
"message": check.message,
|
|
98
|
-
"context": asdict(check.context) if check.context is not None else None, # type: ignore
|
|
99
|
-
"history": [
|
|
100
|
-
{"case": _serialize_case(entry.case), "response": _serialize_response(entry.response)}
|
|
101
|
-
for entry in check.history
|
|
102
|
-
],
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
48
|
def serialize_after_execution(event: events.AfterExecution) -> dict[str, Any] | None:
|
|
107
49
|
return {
|
|
108
50
|
"correlation_id": event.correlation_id,
|
|
@@ -154,45 +96,7 @@ def serialize_stateful_event(event: events.StatefulEvent) -> dict[str, Any] | No
|
|
|
154
96
|
|
|
155
97
|
|
|
156
98
|
def _serialize_stateful_event(event: stateful_events.StatefulEvent) -> dict[str, Any] | None:
|
|
157
|
-
data:
|
|
158
|
-
if isinstance(event, stateful_events.RunStarted):
|
|
159
|
-
data = {
|
|
160
|
-
"timestamp": event.timestamp,
|
|
161
|
-
"started_at": event.started_at,
|
|
162
|
-
}
|
|
163
|
-
elif isinstance(event, stateful_events.SuiteFinished):
|
|
164
|
-
data = {
|
|
165
|
-
"timestamp": event.timestamp,
|
|
166
|
-
"status": event.status,
|
|
167
|
-
"failures": [_serialize_check(SerializedCheck.from_check(failure)) for failure in event.failures],
|
|
168
|
-
}
|
|
169
|
-
elif isinstance(event, stateful_events.Errored):
|
|
170
|
-
data = {
|
|
171
|
-
"timestamp": event.timestamp,
|
|
172
|
-
"exception": format_exception(event.exception, True),
|
|
173
|
-
}
|
|
174
|
-
elif isinstance(event, stateful_events.StepFinished):
|
|
175
|
-
data = {
|
|
176
|
-
"timestamp": event.timestamp,
|
|
177
|
-
"status": event.status,
|
|
178
|
-
"transition_id": {
|
|
179
|
-
"name": event.transition_id.name,
|
|
180
|
-
"status_code": event.transition_id.status_code,
|
|
181
|
-
"source": event.transition_id.source,
|
|
182
|
-
}
|
|
183
|
-
if event.transition_id is not None
|
|
184
|
-
else None,
|
|
185
|
-
"target": event.target,
|
|
186
|
-
"response": {
|
|
187
|
-
"status_code": event.response.status_code,
|
|
188
|
-
"elapsed": event.response.elapsed.total_seconds(),
|
|
189
|
-
}
|
|
190
|
-
if event.response is not None
|
|
191
|
-
else None,
|
|
192
|
-
}
|
|
193
|
-
else:
|
|
194
|
-
data = asdict(event)
|
|
195
|
-
return {"data": {event.__class__.__name__: data}}
|
|
99
|
+
return {"data": {event.__class__.__name__: event.asdict()}}
|
|
196
100
|
|
|
197
101
|
|
|
198
102
|
def serialize_after_stateful_execution(event: events.AfterStatefulExecution) -> dict[str, Any] | None:
|
|
@@ -264,28 +168,3 @@ def serialize_event(
|
|
|
264
168
|
data = merge_recursively(data, extra)
|
|
265
169
|
# Externally tagged structure
|
|
266
170
|
return {event.__class__.__name__: data}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def stringify_path_parameters(path_parameters: dict[str, Any] | None) -> dict[str, str]:
|
|
270
|
-
"""Cast all path parameter values to strings.
|
|
271
|
-
|
|
272
|
-
Path parameter values may be of arbitrary type, but to display them properly they should be casted to strings.
|
|
273
|
-
"""
|
|
274
|
-
return {key: str(value) for key, value in (path_parameters or {}).items()}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def prepare_query(query: dict[str, Any] | None) -> dict[str, list[str]]:
|
|
278
|
-
"""Convert all query values to list of strings.
|
|
279
|
-
|
|
280
|
-
Query parameters may be generated in different shapes, including integers, strings, list of strings, etc.
|
|
281
|
-
It can also be an object, if the schema contains an object, but `style` and `explode` combo is not applicable.
|
|
282
|
-
"""
|
|
283
|
-
|
|
284
|
-
def to_list_of_strings(value: Any) -> list[str]:
|
|
285
|
-
if isinstance(value, list):
|
|
286
|
-
return list(map(str, value))
|
|
287
|
-
if isinstance(value, str):
|
|
288
|
-
return [value]
|
|
289
|
-
return [str(value)]
|
|
290
|
-
|
|
291
|
-
return {key: to_list_of_strings(value) for key, value in (query or {}).items()}
|
|
@@ -11,6 +11,7 @@ from ... import experimental, fixups
|
|
|
11
11
|
from ...code_samples import CodeSampleStyle
|
|
12
12
|
from ...constants import NOT_SET, WAIT_FOR_SCHEMA_INTERVAL
|
|
13
13
|
from ...exceptions import SchemaError, SchemaErrorType
|
|
14
|
+
from ...filters import filter_set_from_components
|
|
14
15
|
from ...generation import (
|
|
15
16
|
DEFAULT_DATA_GENERATION_METHODS,
|
|
16
17
|
DataGenerationMethod,
|
|
@@ -330,17 +331,22 @@ def from_dict(
|
|
|
330
331
|
if rate_limit is not None:
|
|
331
332
|
rate_limiter = build_limiter(rate_limit)
|
|
332
333
|
|
|
334
|
+
filter_set = filter_set_from_components(
|
|
335
|
+
include=True,
|
|
336
|
+
method=method,
|
|
337
|
+
endpoint=endpoint,
|
|
338
|
+
tag=tag,
|
|
339
|
+
operation_id=operation_id,
|
|
340
|
+
skip_deprecated_operations=skip_deprecated_operations,
|
|
341
|
+
)
|
|
342
|
+
|
|
333
343
|
def init_openapi_2() -> SwaggerV20:
|
|
334
344
|
_maybe_validate_schema(raw_schema, definitions.SWAGGER_20_VALIDATOR, validate_schema)
|
|
335
345
|
instance = SwaggerV20(
|
|
336
346
|
raw_schema,
|
|
337
347
|
app=app,
|
|
338
348
|
base_url=base_url,
|
|
339
|
-
|
|
340
|
-
endpoint=endpoint,
|
|
341
|
-
tag=tag,
|
|
342
|
-
operation_id=operation_id,
|
|
343
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
349
|
+
filter_set=filter_set,
|
|
344
350
|
validate_schema=validate_schema,
|
|
345
351
|
data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
|
|
346
352
|
generation_config=generation_config or GenerationConfig(),
|
|
@@ -379,11 +385,7 @@ def from_dict(
|
|
|
379
385
|
raw_schema,
|
|
380
386
|
app=app,
|
|
381
387
|
base_url=base_url,
|
|
382
|
-
|
|
383
|
-
endpoint=endpoint,
|
|
384
|
-
tag=tag,
|
|
385
|
-
operation_id=operation_id,
|
|
386
|
-
skip_deprecated_operations=skip_deprecated_operations,
|
|
388
|
+
filter_set=filter_set,
|
|
387
389
|
validate_schema=validate_schema,
|
|
388
390
|
data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
|
|
389
391
|
generation_config=generation_config or GenerationConfig(),
|
|
@@ -8,6 +8,7 @@ from difflib import get_close_matches
|
|
|
8
8
|
from hashlib import sha1
|
|
9
9
|
from json import JSONDecodeError
|
|
10
10
|
from threading import RLock
|
|
11
|
+
from types import SimpleNamespace
|
|
11
12
|
from typing import (
|
|
12
13
|
TYPE_CHECKING,
|
|
13
14
|
Any,
|
|
@@ -63,13 +64,6 @@ from ._hypothesis import get_case_strategy
|
|
|
63
64
|
from .converter import to_json_schema, to_json_schema_recursive
|
|
64
65
|
from .definitions import OPENAPI_30_VALIDATOR, OPENAPI_31_VALIDATOR, SWAGGER_20_VALIDATOR
|
|
65
66
|
from .examples import get_strategies_from_examples
|
|
66
|
-
from .filters import (
|
|
67
|
-
should_skip_by_operation_id,
|
|
68
|
-
should_skip_by_tag,
|
|
69
|
-
should_skip_deprecated,
|
|
70
|
-
should_skip_endpoint,
|
|
71
|
-
should_skip_method,
|
|
72
|
-
)
|
|
73
67
|
from .parameters import (
|
|
74
68
|
OpenAPI20Body,
|
|
75
69
|
OpenAPI20CompositeBody,
|
|
@@ -149,14 +143,32 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
149
143
|
message += f". Did you mean `{matches[0]}`?"
|
|
150
144
|
raise OperationNotFound(message=message, item=item) from exc
|
|
151
145
|
|
|
152
|
-
def _should_skip(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
146
|
+
def _should_skip(
|
|
147
|
+
self,
|
|
148
|
+
path: str,
|
|
149
|
+
method: str,
|
|
150
|
+
definition: dict[str, Any],
|
|
151
|
+
_ctx_cache: SimpleNamespace = SimpleNamespace(
|
|
152
|
+
operation=APIOperation(
|
|
153
|
+
method="",
|
|
154
|
+
path="",
|
|
155
|
+
verbose_name="",
|
|
156
|
+
definition=OperationDefinition(raw=None, resolved=None, scope=""),
|
|
157
|
+
schema=None, # type: ignore
|
|
158
|
+
)
|
|
159
|
+
),
|
|
160
|
+
) -> bool:
|
|
161
|
+
if method not in HTTP_METHODS:
|
|
162
|
+
return True
|
|
163
|
+
# Attribute assignment is way faster than creating a new namespace every time
|
|
164
|
+
operation = _ctx_cache.operation
|
|
165
|
+
operation.method = method
|
|
166
|
+
operation.path = path
|
|
167
|
+
operation.verbose_name = f"{method.upper()} {path}"
|
|
168
|
+
operation.definition.raw = definition
|
|
169
|
+
operation.definition.resolved = definition
|
|
170
|
+
operation.schema = self
|
|
171
|
+
return not self.filter_set.match(_ctx_cache)
|
|
160
172
|
|
|
161
173
|
def _operation_iter(self) -> Generator[dict[str, Any], None, None]:
|
|
162
174
|
try:
|
|
@@ -164,19 +176,16 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
164
176
|
except KeyError:
|
|
165
177
|
return
|
|
166
178
|
get_full_path = self.get_full_path
|
|
167
|
-
endpoint = self.endpoint
|
|
168
179
|
resolve = self.resolver.resolve
|
|
169
180
|
should_skip = self._should_skip
|
|
170
181
|
for path, path_item in paths.items():
|
|
171
182
|
full_path = get_full_path(path)
|
|
172
|
-
if should_skip_endpoint(full_path, endpoint):
|
|
173
|
-
continue
|
|
174
183
|
try:
|
|
175
184
|
if "$ref" in path_item:
|
|
176
185
|
_, path_item = resolve(path_item["$ref"])
|
|
177
186
|
# Straightforward iteration is faster than converting to a set & calculating length.
|
|
178
187
|
for method, definition in path_item.items():
|
|
179
|
-
if should_skip(method, definition):
|
|
188
|
+
if should_skip(full_path, method, definition):
|
|
180
189
|
continue
|
|
181
190
|
yield definition
|
|
182
191
|
except SCHEMA_PARSING_ERRORS:
|
|
@@ -274,7 +283,6 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
274
283
|
context = HookContext()
|
|
275
284
|
# Optimization: local variables are faster than attribute access
|
|
276
285
|
get_full_path = self.get_full_path
|
|
277
|
-
endpoint = self.endpoint
|
|
278
286
|
dispatch_hook = self.dispatch_hook
|
|
279
287
|
resolve_path_item = self._resolve_path_item
|
|
280
288
|
resolve_shared_parameters = self._resolve_shared_parameters
|
|
@@ -287,8 +295,6 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
287
295
|
method = None
|
|
288
296
|
try:
|
|
289
297
|
full_path = get_full_path(path) # Should be available for later use
|
|
290
|
-
if should_skip_endpoint(full_path, endpoint):
|
|
291
|
-
continue
|
|
292
298
|
dispatch_hook("before_process_path", context, path, path_item)
|
|
293
299
|
scope, path_item = resolve_path_item(path_item)
|
|
294
300
|
with in_scope(self.resolver, scope):
|
|
@@ -298,7 +304,7 @@ class BaseOpenAPISchema(BaseSchema):
|
|
|
298
304
|
continue
|
|
299
305
|
try:
|
|
300
306
|
resolved = resolve_operation(entry)
|
|
301
|
-
if should_skip(method, resolved):
|
|
307
|
+
if should_skip(full_path, method, resolved):
|
|
302
308
|
continue
|
|
303
309
|
parameters = resolved.get("parameters", ())
|
|
304
310
|
parameters = collect_parameters(itertools.chain(parameters, shared_parameters), resolved)
|
schemathesis/stateful/events.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import asdict as _asdict, dataclass
|
|
5
5
|
from enum import Enum
|
|
6
|
-
from typing import TYPE_CHECKING, Type
|
|
6
|
+
from typing import TYPE_CHECKING, Type, Any
|
|
7
|
+
|
|
8
|
+
from ..exceptions import format_exception
|
|
7
9
|
|
|
8
10
|
if TYPE_CHECKING:
|
|
9
11
|
from ..models import Case, Check
|
|
@@ -28,6 +30,9 @@ class StatefulEvent:
|
|
|
28
30
|
|
|
29
31
|
__slots__ = ("timestamp",)
|
|
30
32
|
|
|
33
|
+
def asdict(self) -> dict[str, Any]:
|
|
34
|
+
return _asdict(self)
|
|
35
|
+
|
|
31
36
|
|
|
32
37
|
@dataclass
|
|
33
38
|
class RunStarted(StatefulEvent):
|
|
@@ -43,6 +48,12 @@ class RunStarted(StatefulEvent):
|
|
|
43
48
|
self.started_at = time.time()
|
|
44
49
|
self.timestamp = time.monotonic()
|
|
45
50
|
|
|
51
|
+
def asdict(self) -> dict[str, Any]:
|
|
52
|
+
return {
|
|
53
|
+
"timestamp": self.timestamp,
|
|
54
|
+
"started_at": self.started_at,
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
|
|
47
58
|
@dataclass
|
|
48
59
|
class RunFinished(StatefulEvent):
|
|
@@ -90,6 +101,15 @@ class SuiteFinished(StatefulEvent):
|
|
|
90
101
|
self.failures = failures
|
|
91
102
|
self.timestamp = time.monotonic()
|
|
92
103
|
|
|
104
|
+
def asdict(self) -> dict[str, Any]:
|
|
105
|
+
from ..runner.serialization import SerializedCheck, _serialize_check
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"timestamp": self.timestamp,
|
|
109
|
+
"status": self.status,
|
|
110
|
+
"failures": [_serialize_check(SerializedCheck.from_check(failure)) for failure in self.failures],
|
|
111
|
+
}
|
|
112
|
+
|
|
93
113
|
|
|
94
114
|
class ScenarioStatus(str, Enum):
|
|
95
115
|
"""Status of a single scenario execution."""
|
|
@@ -203,6 +223,26 @@ class StepFinished(StatefulEvent):
|
|
|
203
223
|
self.checks = checks
|
|
204
224
|
self.timestamp = time.monotonic()
|
|
205
225
|
|
|
226
|
+
def asdict(self) -> dict[str, Any]:
|
|
227
|
+
return {
|
|
228
|
+
"timestamp": self.timestamp,
|
|
229
|
+
"status": self.status,
|
|
230
|
+
"transition_id": {
|
|
231
|
+
"name": self.transition_id.name,
|
|
232
|
+
"status_code": self.transition_id.status_code,
|
|
233
|
+
"source": self.transition_id.source,
|
|
234
|
+
}
|
|
235
|
+
if self.transition_id is not None
|
|
236
|
+
else None,
|
|
237
|
+
"target": self.target,
|
|
238
|
+
"response": {
|
|
239
|
+
"status_code": self.response.status_code,
|
|
240
|
+
"elapsed": self.response.elapsed.total_seconds(),
|
|
241
|
+
}
|
|
242
|
+
if self.response is not None
|
|
243
|
+
else None,
|
|
244
|
+
}
|
|
245
|
+
|
|
206
246
|
|
|
207
247
|
@dataclass
|
|
208
248
|
class Interrupted(StatefulEvent):
|
|
@@ -225,3 +265,9 @@ class Errored(StatefulEvent):
|
|
|
225
265
|
def __init__(self, *, exception: Exception) -> None:
|
|
226
266
|
self.exception = exception
|
|
227
267
|
self.timestamp = time.monotonic()
|
|
268
|
+
|
|
269
|
+
def asdict(self) -> dict[str, Any]:
|
|
270
|
+
return {
|
|
271
|
+
"timestamp": self.timestamp,
|
|
272
|
+
"exception": format_exception(self.exception, True),
|
|
273
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.32.
|
|
3
|
+
Version: 3.32.1
|
|
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
|
|
@@ -12,16 +12,16 @@ schemathesis/code_samples.py,sha256=xk1-1jnXg5hS40VzIZp8PEtZwGaazNlVKMT7_X-zG-M,
|
|
|
12
12
|
schemathesis/constants.py,sha256=l1YQ7PXhEj9dyf9CTESVUpPOaFCH7iz-Fe8o4v6Th_s,2673
|
|
13
13
|
schemathesis/exceptions.py,sha256=w-5A-2Yb6EJOSvPycbx79T9Lr1IcMWJ8UFq6DW9kIMc,19975
|
|
14
14
|
schemathesis/failures.py,sha256=wXz5Kr5i-ojcYc-BdzFlNbNGOfoVXHZM6kd4iULdHK4,7003
|
|
15
|
-
schemathesis/filters.py,sha256=
|
|
15
|
+
schemathesis/filters.py,sha256=02mg5Gn32AkOprfk33wjH8oXmbsYpogN8XGlrHkEo8Q,14265
|
|
16
16
|
schemathesis/graphql.py,sha256=YkoKWY5K8lxp7H3ikAs-IsoDbiPwJvChG7O8p3DgwtI,229
|
|
17
17
|
schemathesis/hooks.py,sha256=dveqMmThIvt4fDahUXhU2nCq5pFvYjzzd1Ys_MhrJZA,12398
|
|
18
|
-
schemathesis/lazy.py,sha256=
|
|
18
|
+
schemathesis/lazy.py,sha256=yEMQXbve2cB5mX-9Kv9npUECYfXVMc-dK33k82WJVcM,15405
|
|
19
19
|
schemathesis/loaders.py,sha256=OtCD1o0TVmSNAUF7dgHpouoAXtY6w9vEtsRVGv4lE0g,4588
|
|
20
20
|
schemathesis/models.py,sha256=nbm9Agqw94RYib2Q4OH7iOiEPDGw64NlQnEB7o5Spio,44925
|
|
21
21
|
schemathesis/parameters.py,sha256=PndmqQRlEYsCt1kWjSShPsFf6vj7X_7FRdz_-A95eNg,2258
|
|
22
22
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
schemathesis/sanitization.py,sha256=mRR4YvXpzqbmgX8Xu6rume6LBcz9g_oyusvbesZl44I,8958
|
|
24
|
-
schemathesis/schemas.py,sha256=
|
|
24
|
+
schemathesis/schemas.py,sha256=wfcZflxe6ITt6s1-d37RwoRHJaYckWS76T-_PBCC7hI,17757
|
|
25
25
|
schemathesis/serializers.py,sha256=kxXZ-UGa1v_vOm0sC4QYcrNv4rfvI7tHGT2elRVbCbc,11649
|
|
26
26
|
schemathesis/targets.py,sha256=XIGRghvEzbmEJjse9aZgNEj67L3jAbiazm2rxURWgDE,2351
|
|
27
27
|
schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
|
|
@@ -69,9 +69,9 @@ schemathesis/internal/result.py,sha256=d449YvyONjqjDs-A5DAPgtAI96iT753K8sU6_1HLo
|
|
|
69
69
|
schemathesis/internal/transformation.py,sha256=3S6AzAqdsEsB5iobFgSuvL0UMUqH0JHC7hGxKwcpqPw,450
|
|
70
70
|
schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QAtMPyEw,966
|
|
71
71
|
schemathesis/runner/__init__.py,sha256=mXFJTAjbjfGImIOB5d1rkpZC5TtVRxSf_SMiBEhKNMI,21350
|
|
72
|
-
schemathesis/runner/events.py,sha256=
|
|
72
|
+
schemathesis/runner/events.py,sha256=ljwDjv-4hLS4MPfdfYDB2EAxkJR_67-fVvMu-pJU-yk,11498
|
|
73
73
|
schemathesis/runner/probes.py,sha256=no5AfO3kse25qvHevjeUfB0Q3C860V2AYzschUW3QMQ,5688
|
|
74
|
-
schemathesis/runner/serialization.py,sha256=
|
|
74
|
+
schemathesis/runner/serialization.py,sha256=erbXEHyI8rIlkQ42AwgmlH7aAbh313EPqCEfrGKxUls,20040
|
|
75
75
|
schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
|
|
76
76
|
schemathesis/runner/impl/core.py,sha256=k1c2QMMoqtXk1bafVRxJwGnIxTQxU0Pr1Am0Xo9WPd4,45063
|
|
77
77
|
schemathesis/runner/impl/solo.py,sha256=N7-pUL6nWGiSRUC4Zqy1T4h99vbeQowP6b6cMnobOow,3042
|
|
@@ -87,7 +87,7 @@ schemathesis/service/hosts.py,sha256=ad2Lxq9Zcc9PP-1eFLQnxen4ImglcGOH8n7CGG72NNg
|
|
|
87
87
|
schemathesis/service/metadata.py,sha256=x2LeCED1mdPf-YQJmjY8xtcIKHfD1ap5V0BGl-UgqNo,2087
|
|
88
88
|
schemathesis/service/models.py,sha256=ihItUJ9CvH4TvmdfJY3W88NR82OODF8a3RD7WRXn6RM,6578
|
|
89
89
|
schemathesis/service/report.py,sha256=4A8nf6_KOjDW3x1VXF8gSf_WY2xXp1Cbz-Owl_GeR7o,8294
|
|
90
|
-
schemathesis/service/serialization.py,sha256=
|
|
90
|
+
schemathesis/service/serialization.py,sha256=QX7-wbh3vvUrZtRQWZkwaIUSIkcPtD-RMNFmHBY8J5s,6196
|
|
91
91
|
schemathesis/service/usage.py,sha256=UbXqxeDq5mAjKkfV4hApZsReZmQHXiqoXUYn_Z6YuZk,2438
|
|
92
92
|
schemathesis/specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
93
93
|
schemathesis/specs/graphql/__init__.py,sha256=fgyHtvWNUVWismBTOqxQtgLoTighTfvMv6v6QCD_Oyc,85
|
|
@@ -105,14 +105,13 @@ schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnK
|
|
|
105
105
|
schemathesis/specs/openapi/converter.py,sha256=TaYgc5BBHPdkN-n0lqpbeVgLu3eL3L8Wu3y_Vo3TJaQ,2800
|
|
106
106
|
schemathesis/specs/openapi/definitions.py,sha256=Z186F0gNBSCmPg-Kk7Q-n6XxEZHIOzgUyeqixlC62XE,94058
|
|
107
107
|
schemathesis/specs/openapi/examples.py,sha256=5bjmW3BnJVTiLlWZbimdfOzQQFR6m1P9G0FErr9g3WI,15128
|
|
108
|
-
schemathesis/specs/openapi/filters.py,sha256=6Q9eNQ6zCR-NQkUxgnkSDWxfk3hsZuxemBv7v1rhwb4,1437
|
|
109
108
|
schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
|
|
110
109
|
schemathesis/specs/openapi/links.py,sha256=2ucOLs50OhCqu0PEdbT_BGUM3fKnHBl97YGISLpAxLY,16023
|
|
111
|
-
schemathesis/specs/openapi/loaders.py,sha256=
|
|
110
|
+
schemathesis/specs/openapi/loaders.py,sha256=NOVG8Jrl9pv5O64wAgWHrco1okpRzQDamHDnidZdIwY,24905
|
|
112
111
|
schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
|
|
113
112
|
schemathesis/specs/openapi/parameters.py,sha256=_6vNCnPXcdxjfAQbykCRLHjvmTpu_02xDJghxDrGYr8,13611
|
|
114
113
|
schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
|
|
115
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
|
114
|
+
schemathesis/specs/openapi/schemas.py,sha256=EvdW-aJ-OY-zdSU3AbgK1TydsEJueWMfCZRIfAyMUbI,52420
|
|
116
115
|
schemathesis/specs/openapi/security.py,sha256=nEhDB_SvEFldmfpa9uOQywfWN6DtXHKmgtwucJvfN5Q,7096
|
|
117
116
|
schemathesis/specs/openapi/serialization.py,sha256=5qGdFHZ3n80UlbSXrO_bkr4Al_7ci_Z3aSUjZczNDQY,11384
|
|
118
117
|
schemathesis/specs/openapi/utils.py,sha256=-TCu0hTrlwp2x5qHNp-TxiHRMeIZC9OBmlhLssjRIiQ,742
|
|
@@ -134,7 +133,7 @@ schemathesis/specs/openapi/stateful/types.py,sha256=UuGcCTFvaHsqeLN9ZeUNcbjsEwmt
|
|
|
134
133
|
schemathesis/stateful/__init__.py,sha256=qyQJ-9Ect-AWZiAsK63F3BTGu-jZnPCOp1q46YAonkQ,4911
|
|
135
134
|
schemathesis/stateful/config.py,sha256=rtGl3egoUuPFxrWcl5xZj_6KmKzZyYaC0b_AUotvurs,2930
|
|
136
135
|
schemathesis/stateful/context.py,sha256=MeP3-lKyhtAd-jzApC65AWlDZSOBzQq0IgK-nvagYqs,4519
|
|
137
|
-
schemathesis/stateful/events.py,sha256=
|
|
136
|
+
schemathesis/stateful/events.py,sha256=3AANEwTZFuW9Gbcig70NvdT_h4o5r-Esx9Hpm7zphdg,6710
|
|
138
137
|
schemathesis/stateful/runner.py,sha256=vFd8Id3zzSQVHBK9UgInBpBT7oyUNagQ0p7ZJnPyHRk,10969
|
|
139
138
|
schemathesis/stateful/sink.py,sha256=xjsqJYH5WETKh5pDGlchYyjT3HcjzHEotUjvo1p0JsE,2470
|
|
140
139
|
schemathesis/stateful/state_machine.py,sha256=iRbznWxHnUdLhMpiBaHxe6Nh1EacyGnGFz4DCRwV5j4,12228
|
|
@@ -145,8 +144,8 @@ schemathesis/transports/auth.py,sha256=yELjkEkfx4g74hNrd0Db9aFf0xDJDRIwhg2vzKOTZ
|
|
|
145
144
|
schemathesis/transports/content_types.py,sha256=VrcRQvF5T_TUjrCyrZcYF2LOwKfs3IrLcMtkVSp1ImI,2189
|
|
146
145
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
|
147
146
|
schemathesis/transports/responses.py,sha256=6-gvVcRK0Ho_lSydUysBNFWoJwZEiEgf6Iv-GWkQGd8,1675
|
|
148
|
-
schemathesis-3.32.
|
|
149
|
-
schemathesis-3.32.
|
|
150
|
-
schemathesis-3.32.
|
|
151
|
-
schemathesis-3.32.
|
|
152
|
-
schemathesis-3.32.
|
|
147
|
+
schemathesis-3.32.1.dist-info/METADATA,sha256=LN3HsIvSIqjk0_UQP7qzHHe9tHceThbE-wu9L15SiF0,17795
|
|
148
|
+
schemathesis-3.32.1.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
|
|
149
|
+
schemathesis-3.32.1.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
150
|
+
schemathesis-3.32.1.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
151
|
+
schemathesis-3.32.1.dist-info/RECORD,,
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import re
|
|
4
|
-
|
|
5
|
-
from ...types import Filter
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def should_skip_method(method: str, pattern: Filter | None) -> bool:
|
|
9
|
-
if pattern is None:
|
|
10
|
-
return False
|
|
11
|
-
patterns = _ensure_tuple(pattern)
|
|
12
|
-
return method.upper() not in map(str.upper, patterns)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def should_skip_endpoint(endpoint: str, pattern: Filter | None) -> bool:
|
|
16
|
-
if pattern is None:
|
|
17
|
-
return False
|
|
18
|
-
return not _match_any_pattern(endpoint, pattern)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def should_skip_by_tag(tags: list[str] | None, pattern: Filter | None) -> bool:
|
|
22
|
-
if pattern is None:
|
|
23
|
-
return False
|
|
24
|
-
if not tags:
|
|
25
|
-
return True
|
|
26
|
-
patterns = _ensure_tuple(pattern)
|
|
27
|
-
return not any(re.search(item, tag) for item in patterns for tag in tags)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def should_skip_by_operation_id(operation_id: str | None, pattern: Filter | None) -> bool:
|
|
31
|
-
if pattern is None:
|
|
32
|
-
return False
|
|
33
|
-
if not operation_id:
|
|
34
|
-
return True
|
|
35
|
-
return not _match_any_pattern(operation_id, pattern)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def should_skip_deprecated(is_deprecated: bool, skip_deprecated_operations: bool) -> bool:
|
|
39
|
-
return skip_deprecated_operations and is_deprecated
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _match_any_pattern(target: str, pattern: Filter) -> bool:
|
|
43
|
-
patterns = _ensure_tuple(pattern)
|
|
44
|
-
return any(re.search(item, target) for item in patterns)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _ensure_tuple(item: Filter) -> list | set | tuple:
|
|
48
|
-
if not isinstance(item, (list, set, tuple)):
|
|
49
|
-
return (item,)
|
|
50
|
-
return item
|
|
File without changes
|
|
File without changes
|
|
File without changes
|