schemathesis 3.39.9__py3-none-any.whl → 3.39.11__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/_hypothesis.py +194 -54
- schemathesis/cli/output/default.py +0 -16
- schemathesis/constants.py +0 -2
- schemathesis/failures.py +6 -0
- schemathesis/internal/checks.py +3 -1
- schemathesis/runner/impl/context.py +0 -16
- schemathesis/runner/impl/core.py +39 -5
- schemathesis/specs/openapi/checks.py +6 -1
- schemathesis/specs/openapi/examples.py +3 -5
- schemathesis/specs/openapi/patterns.py +18 -3
- schemathesis/specs/openapi/serialization.py +12 -0
- {schemathesis-3.39.9.dist-info → schemathesis-3.39.11.dist-info}/METADATA +1 -1
- {schemathesis-3.39.9.dist-info → schemathesis-3.39.11.dist-info}/RECORD +16 -16
- {schemathesis-3.39.9.dist-info → schemathesis-3.39.11.dist-info}/WHEEL +0 -0
- {schemathesis-3.39.9.dist-info → schemathesis-3.39.11.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.39.9.dist-info → schemathesis-3.39.11.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import asyncio
|
6
|
-
import
|
6
|
+
from dataclasses import dataclass
|
7
7
|
import warnings
|
8
8
|
from functools import wraps
|
9
9
|
from itertools import combinations
|
@@ -15,8 +15,10 @@ from hypothesis.errors import HypothesisWarning, Unsatisfiable
|
|
15
15
|
from hypothesis.internal.entropy import deterministic_PRNG
|
16
16
|
from jsonschema.exceptions import SchemaError
|
17
17
|
|
18
|
+
from schemathesis.serializers import get_first_matching_media_type
|
19
|
+
|
18
20
|
from . import _patches
|
19
|
-
from .auths import get_auth_storage_from_test
|
21
|
+
from .auths import AuthStorage, get_auth_storage_from_test
|
20
22
|
from .constants import DEFAULT_DEADLINE, NOT_SET
|
21
23
|
from .exceptions import OperationSchemaError, SerializationNotPossible
|
22
24
|
from .experimental import COVERAGE_PHASE
|
@@ -27,6 +29,7 @@ from .parameters import ParameterSet
|
|
27
29
|
from .transports.content_types import parse_content_type
|
28
30
|
from .transports.headers import has_invalid_characters, is_latin_1_encodable
|
29
31
|
from .types import NotSet
|
32
|
+
from schemathesis import auths
|
30
33
|
|
31
34
|
if TYPE_CHECKING:
|
32
35
|
from .utils import GivenInput
|
@@ -111,7 +114,9 @@ def create_test(
|
|
111
114
|
wrapped_test, operation, hook_dispatcher=hook_dispatcher, as_strategy_kwargs=as_strategy_kwargs
|
112
115
|
)
|
113
116
|
if COVERAGE_PHASE.is_enabled:
|
114
|
-
wrapped_test = add_coverage(
|
117
|
+
wrapped_test = add_coverage(
|
118
|
+
wrapped_test, operation, data_generation_methods, auth_storage, as_strategy_kwargs
|
119
|
+
)
|
115
120
|
return wrapped_test
|
116
121
|
|
117
122
|
|
@@ -215,31 +220,158 @@ def adjust_urlencoded_payload(case: Case) -> None:
|
|
215
220
|
|
216
221
|
|
217
222
|
def add_coverage(
|
218
|
-
test: Callable,
|
223
|
+
test: Callable,
|
224
|
+
operation: APIOperation,
|
225
|
+
data_generation_methods: list[DataGenerationMethod],
|
226
|
+
auth_storage: AuthStorage | None,
|
227
|
+
as_strategy_kwargs: dict[str, Any],
|
219
228
|
) -> Callable:
|
220
|
-
|
221
|
-
|
222
|
-
|
229
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
230
|
+
|
231
|
+
auth_context = auths.AuthContext(
|
232
|
+
operation=operation,
|
233
|
+
app=operation.app,
|
234
|
+
)
|
235
|
+
overrides = {
|
236
|
+
container: as_strategy_kwargs[container]
|
237
|
+
for container in LOCATION_TO_CONTAINER.values()
|
238
|
+
if container in as_strategy_kwargs
|
239
|
+
}
|
240
|
+
for case in _iter_coverage_cases(operation, data_generation_methods):
|
241
|
+
if case.media_type and get_first_matching_media_type(case.media_type) is None:
|
242
|
+
continue
|
243
|
+
adjust_urlencoded_payload(case)
|
244
|
+
auths.set_on_case(case, auth_context, auth_storage)
|
245
|
+
for container_name, value in overrides.items():
|
246
|
+
container = getattr(case, container_name)
|
247
|
+
if container is None:
|
248
|
+
setattr(case, container_name, value)
|
249
|
+
else:
|
250
|
+
container.update(value)
|
251
|
+
test = hypothesis.example(case=case)(test)
|
223
252
|
return test
|
224
253
|
|
225
254
|
|
255
|
+
class Template:
|
256
|
+
__slots__ = ("_components", "_template", "_serializers")
|
257
|
+
|
258
|
+
def __init__(self, serializers: dict[str, Callable]) -> None:
|
259
|
+
self._components: dict[str, DataGenerationMethod] = {}
|
260
|
+
self._template: dict[str, Any] = {}
|
261
|
+
self._serializers = serializers
|
262
|
+
|
263
|
+
def __contains__(self, key: str) -> bool:
|
264
|
+
return key in self._template
|
265
|
+
|
266
|
+
def __getitem__(self, key: str) -> dict:
|
267
|
+
return self._template[key]
|
268
|
+
|
269
|
+
def get(self, key: str, default: Any = None) -> dict:
|
270
|
+
return self._template.get(key, default)
|
271
|
+
|
272
|
+
def add_parameter(self, location: str, name: str, value: coverage.GeneratedValue) -> None:
|
273
|
+
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
274
|
+
|
275
|
+
component_name = LOCATION_TO_CONTAINER[location]
|
276
|
+
method = self._components.get(component_name)
|
277
|
+
if method is None:
|
278
|
+
self._components[component_name] = value.data_generation_method
|
279
|
+
elif value.data_generation_method == DataGenerationMethod.negative:
|
280
|
+
self._components[component_name] = DataGenerationMethod.negative
|
281
|
+
|
282
|
+
container = self._template.setdefault(component_name, {})
|
283
|
+
container[name] = value.value
|
284
|
+
|
285
|
+
def set_body(self, body: coverage.GeneratedValue, media_type: str) -> None:
|
286
|
+
self._template["body"] = body.value
|
287
|
+
self._template["media_type"] = media_type
|
288
|
+
self._components["body"] = body.data_generation_method
|
289
|
+
|
290
|
+
def _serialize(self, kwargs: dict[str, Any]) -> dict[str, Any]:
|
291
|
+
from schemathesis.specs.openapi._hypothesis import quote_all
|
292
|
+
|
293
|
+
output = {}
|
294
|
+
for container_name, value in kwargs.items():
|
295
|
+
serializer = self._serializers.get(container_name)
|
296
|
+
if container_name in ("headers", "cookies") and isinstance(value, dict):
|
297
|
+
value = _stringify_value(value, container_name)
|
298
|
+
if serializer is not None:
|
299
|
+
value = serializer(value)
|
300
|
+
if container_name == "query" and isinstance(value, dict):
|
301
|
+
value = _stringify_value(value, container_name)
|
302
|
+
if container_name == "path_parameters" and isinstance(value, dict):
|
303
|
+
value = _stringify_value(quote_all(value), container_name)
|
304
|
+
output[container_name] = value
|
305
|
+
return output
|
306
|
+
|
307
|
+
def unmodified(self) -> TemplateValue:
|
308
|
+
kwargs = self._template.copy()
|
309
|
+
kwargs = self._serialize(kwargs)
|
310
|
+
return TemplateValue(kwargs=kwargs, components=self._components.copy())
|
311
|
+
|
312
|
+
def with_body(self, *, media_type: str, value: coverage.GeneratedValue) -> TemplateValue:
|
313
|
+
kwargs = {**self._template, "media_type": media_type, "body": value.value}
|
314
|
+
kwargs = self._serialize(kwargs)
|
315
|
+
components = {**self._components, "body": value.data_generation_method}
|
316
|
+
return TemplateValue(kwargs=kwargs, components=components)
|
317
|
+
|
318
|
+
def with_parameter(self, *, location: str, name: str, value: coverage.GeneratedValue) -> TemplateValue:
|
319
|
+
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
320
|
+
|
321
|
+
container_name = LOCATION_TO_CONTAINER[location]
|
322
|
+
container = self._template[container_name]
|
323
|
+
return self.with_container(
|
324
|
+
container_name=container_name,
|
325
|
+
value={**container, name: value.value},
|
326
|
+
data_generation_method=value.data_generation_method,
|
327
|
+
)
|
328
|
+
|
329
|
+
def with_container(
|
330
|
+
self, *, container_name: str, value: Any, data_generation_method: DataGenerationMethod
|
331
|
+
) -> TemplateValue:
|
332
|
+
kwargs = {**self._template, container_name: value}
|
333
|
+
kwargs = self._serialize(kwargs)
|
334
|
+
components = {**self._components, container_name: data_generation_method}
|
335
|
+
return TemplateValue(kwargs=kwargs, components=components)
|
336
|
+
|
337
|
+
|
338
|
+
@dataclass
|
339
|
+
class TemplateValue:
|
340
|
+
kwargs: dict[str, Any]
|
341
|
+
components: dict[str, DataGenerationMethod]
|
342
|
+
__slots__ = ("kwargs", "components")
|
343
|
+
|
344
|
+
|
345
|
+
def _stringify_value(val: Any, container_name: str) -> Any:
|
346
|
+
if val is None:
|
347
|
+
return "null"
|
348
|
+
if val is True:
|
349
|
+
return "true"
|
350
|
+
if val is False:
|
351
|
+
return "false"
|
352
|
+
if isinstance(val, (int, float)):
|
353
|
+
return str(val)
|
354
|
+
if isinstance(val, list):
|
355
|
+
if container_name == "query":
|
356
|
+
# Having a list here ensures there will be multiple query parameters wit the same name
|
357
|
+
return [_stringify_value(item, container_name) for item in val]
|
358
|
+
# use comma-separated values style for arrays
|
359
|
+
return ",".join(_stringify_value(sub, container_name) for sub in val)
|
360
|
+
if isinstance(val, dict):
|
361
|
+
return {key: _stringify_value(sub, container_name) for key, sub in val.items()}
|
362
|
+
return val
|
363
|
+
|
364
|
+
|
226
365
|
def _iter_coverage_cases(
|
227
366
|
operation: APIOperation, data_generation_methods: list[DataGenerationMethod]
|
228
367
|
) -> Generator[Case, None, None]:
|
229
368
|
from .specs.openapi.constants import LOCATION_TO_CONTAINER
|
230
369
|
from .specs.openapi.examples import find_in_responses, find_matching_in_responses
|
231
|
-
|
232
|
-
def _stringify_value(val: Any, location: str) -> str | list[str]:
|
233
|
-
if isinstance(val, list):
|
234
|
-
if location == "query":
|
235
|
-
# Having a list here ensures there will be multiple query parameters wit the same name
|
236
|
-
return [json.dumps(item) for item in val]
|
237
|
-
# use comma-separated values style for arrays
|
238
|
-
return ",".join(json.dumps(sub) for sub in val)
|
239
|
-
return json.dumps(val)
|
370
|
+
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
240
371
|
|
241
372
|
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
242
|
-
|
373
|
+
serializers = get_serializers_for_operation(operation)
|
374
|
+
template = Template(serializers)
|
243
375
|
responses = find_in_responses(operation)
|
244
376
|
for parameter in operation.iter_parameters():
|
245
377
|
location = parameter.location
|
@@ -253,11 +385,7 @@ def _iter_coverage_cases(
|
|
253
385
|
value = next(gen, NOT_SET)
|
254
386
|
if isinstance(value, NotSet):
|
255
387
|
continue
|
256
|
-
|
257
|
-
if location in ("header", "cookie", "path", "query") and not isinstance(value.value, str):
|
258
|
-
container[name] = _stringify_value(value.value, location)
|
259
|
-
else:
|
260
|
-
container[name] = value.value
|
388
|
+
template.add_parameter(location, name, value)
|
261
389
|
generators[(location, name)] = gen
|
262
390
|
if operation.body:
|
263
391
|
for body in operation.body:
|
@@ -274,48 +402,48 @@ def _iter_coverage_cases(
|
|
274
402
|
if isinstance(value, NotSet):
|
275
403
|
continue
|
276
404
|
if "body" not in template:
|
277
|
-
template
|
278
|
-
|
279
|
-
case = operation.make_case(**
|
405
|
+
template.set_body(value, body.media_type)
|
406
|
+
data = template.with_body(value=value, media_type=body.media_type)
|
407
|
+
case = operation.make_case(**data.kwargs)
|
280
408
|
case.data_generation_method = value.data_generation_method
|
281
409
|
case.meta = _make_meta(
|
282
410
|
description=value.description,
|
283
411
|
location=value.location,
|
284
412
|
parameter=body.media_type,
|
285
413
|
parameter_location="body",
|
414
|
+
**data.components,
|
286
415
|
)
|
287
416
|
yield case
|
288
417
|
for next_value in gen:
|
289
|
-
|
418
|
+
data = template.with_body(value=next_value, media_type=body.media_type)
|
419
|
+
case = operation.make_case(**data.kwargs)
|
290
420
|
case.data_generation_method = next_value.data_generation_method
|
291
421
|
case.meta = _make_meta(
|
292
422
|
description=next_value.description,
|
293
423
|
location=next_value.location,
|
294
424
|
parameter=body.media_type,
|
295
425
|
parameter_location="body",
|
426
|
+
**data.components,
|
296
427
|
)
|
297
428
|
yield case
|
298
429
|
elif DataGenerationMethod.positive in data_generation_methods:
|
299
|
-
|
430
|
+
data = template.unmodified()
|
431
|
+
case = operation.make_case(**data.kwargs)
|
300
432
|
case.data_generation_method = DataGenerationMethod.positive
|
301
|
-
case.meta = _make_meta(description="Default positive test case")
|
433
|
+
case.meta = _make_meta(description="Default positive test case", **data.components)
|
302
434
|
yield case
|
303
435
|
|
304
436
|
for (location, name), gen in generators.items():
|
305
|
-
container_name = LOCATION_TO_CONTAINER[location]
|
306
|
-
container = template[container_name]
|
307
437
|
for value in gen:
|
308
|
-
|
309
|
-
|
310
|
-
else:
|
311
|
-
generated = value.value
|
312
|
-
case = operation.make_case(**{**template, container_name: {**container, name: generated}})
|
438
|
+
data = template.with_parameter(location=location, name=name, value=value)
|
439
|
+
case = operation.make_case(**data.kwargs)
|
313
440
|
case.data_generation_method = value.data_generation_method
|
314
441
|
case.meta = _make_meta(
|
315
442
|
description=value.description,
|
316
443
|
location=value.location,
|
317
444
|
parameter=name,
|
318
445
|
parameter_location=location,
|
446
|
+
**data.components,
|
319
447
|
)
|
320
448
|
yield case
|
321
449
|
if DataGenerationMethod.negative in data_generation_methods:
|
@@ -323,10 +451,11 @@ def _iter_coverage_cases(
|
|
323
451
|
# NOTE: The HEAD method is excluded
|
324
452
|
methods = {"get", "put", "post", "delete", "options", "patch", "trace"} - set(operation.schema[operation.path])
|
325
453
|
for method in sorted(methods):
|
326
|
-
|
454
|
+
data = template.unmodified()
|
455
|
+
case = operation.make_case(**data.kwargs)
|
327
456
|
case._explicit_method = method
|
328
457
|
case.data_generation_method = DataGenerationMethod.negative
|
329
|
-
case.meta = _make_meta(description=f"Unspecified HTTP method: {method.upper()}")
|
458
|
+
case.meta = _make_meta(description=f"Unspecified HTTP method: {method.upper()}", **data.components)
|
330
459
|
yield case
|
331
460
|
# Generate duplicate query parameters
|
332
461
|
if operation.query:
|
@@ -336,13 +465,19 @@ def _iter_coverage_cases(
|
|
336
465
|
# I.e. contains just `default` value without any other keywords
|
337
466
|
value = container.get(parameter.name, NOT_SET)
|
338
467
|
if value is not NOT_SET:
|
339
|
-
|
468
|
+
data = template.with_container(
|
469
|
+
container_name="query",
|
470
|
+
value={**container, parameter.name: [value, value]},
|
471
|
+
data_generation_method=DataGenerationMethod.negative,
|
472
|
+
)
|
473
|
+
case = operation.make_case(**data.kwargs)
|
340
474
|
case.data_generation_method = DataGenerationMethod.negative
|
341
475
|
case.meta = _make_meta(
|
342
476
|
description=f"Duplicate `{parameter.name}` query parameter",
|
343
477
|
location=None,
|
344
478
|
parameter=parameter.name,
|
345
479
|
parameter_location="query",
|
480
|
+
**data.components,
|
346
481
|
)
|
347
482
|
yield case
|
348
483
|
# Generate missing required parameters
|
@@ -352,15 +487,19 @@ def _iter_coverage_cases(
|
|
352
487
|
location = parameter.location
|
353
488
|
container_name = LOCATION_TO_CONTAINER[location]
|
354
489
|
container = template[container_name]
|
355
|
-
|
356
|
-
|
490
|
+
data = template.with_container(
|
491
|
+
container_name=container_name,
|
492
|
+
value={k: v for k, v in container.items() if k != name},
|
493
|
+
data_generation_method=DataGenerationMethod.negative,
|
357
494
|
)
|
495
|
+
case = operation.make_case(**data.kwargs)
|
358
496
|
case.data_generation_method = DataGenerationMethod.negative
|
359
497
|
case.meta = _make_meta(
|
360
498
|
description=f"Missing `{name}` at {location}",
|
361
499
|
location=None,
|
362
500
|
parameter=name,
|
363
501
|
parameter_location=location,
|
502
|
+
**data.components,
|
364
503
|
)
|
365
504
|
yield case
|
366
505
|
# Generate combinations for each location
|
@@ -389,21 +528,17 @@ def _iter_coverage_cases(
|
|
389
528
|
_parameter: str | None,
|
390
529
|
_data_generation_method: DataGenerationMethod,
|
391
530
|
) -> Case:
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
}
|
397
|
-
else:
|
398
|
-
container = container_values
|
399
|
-
|
400
|
-
case = operation.make_case(**{**template, _container_name: container})
|
531
|
+
data = template.with_container(
|
532
|
+
container_name=_container_name, value=container_values, data_generation_method=_data_generation_method
|
533
|
+
)
|
534
|
+
case = operation.make_case(**data.kwargs)
|
401
535
|
case.data_generation_method = _data_generation_method
|
402
536
|
case.meta = _make_meta(
|
403
537
|
description=description,
|
404
538
|
location=None,
|
405
539
|
parameter=_parameter,
|
406
540
|
parameter_location=_location,
|
541
|
+
**data.components,
|
407
542
|
)
|
408
543
|
return case
|
409
544
|
|
@@ -496,13 +631,18 @@ def _make_meta(
|
|
496
631
|
location: str | None = None,
|
497
632
|
parameter: str | None = None,
|
498
633
|
parameter_location: str | None = None,
|
634
|
+
query: DataGenerationMethod | None = None,
|
635
|
+
path_parameters: DataGenerationMethod | None = None,
|
636
|
+
headers: DataGenerationMethod | None = None,
|
637
|
+
cookies: DataGenerationMethod | None = None,
|
638
|
+
body: DataGenerationMethod | None = None,
|
499
639
|
) -> GenerationMetadata:
|
500
640
|
return GenerationMetadata(
|
501
|
-
query=
|
502
|
-
path_parameters=
|
503
|
-
headers=
|
504
|
-
cookies=
|
505
|
-
body=
|
641
|
+
query=query,
|
642
|
+
path_parameters=path_parameters,
|
643
|
+
headers=headers,
|
644
|
+
cookies=cookies,
|
645
|
+
body=body,
|
506
646
|
phase=TestPhase.COVERAGE,
|
507
647
|
description=description,
|
508
648
|
location=location,
|
@@ -14,11 +14,8 @@ import click
|
|
14
14
|
from ... import experimental, service
|
15
15
|
from ...constants import (
|
16
16
|
DISCORD_LINK,
|
17
|
-
FALSE_VALUES,
|
18
17
|
FLAKY_FAILURE_MESSAGE,
|
19
|
-
GITHUB_APP_LINK,
|
20
18
|
ISSUE_TRACKER_URL,
|
21
|
-
REPORT_SUGGESTION_ENV_VAR,
|
22
19
|
SCHEMATHESIS_TEST_CASE_HEADER,
|
23
20
|
SCHEMATHESIS_VERSION,
|
24
21
|
)
|
@@ -499,19 +496,6 @@ def display_statistic(context: ExecutionContext, event: events.Finished) -> None
|
|
499
496
|
elif isinstance(context.report, ServiceReportContext):
|
500
497
|
click.echo()
|
501
498
|
handle_service_integration(context.report)
|
502
|
-
else:
|
503
|
-
env_var = os.getenv(REPORT_SUGGESTION_ENV_VAR)
|
504
|
-
if env_var is not None and env_var.lower() in FALSE_VALUES:
|
505
|
-
return
|
506
|
-
click.echo(
|
507
|
-
f"\n{bold('Tip')}: Use the {bold('`--report`')} CLI option to visualize test results via Schemathesis.io.\n"
|
508
|
-
"We run additional conformance checks on reports from public repos."
|
509
|
-
)
|
510
|
-
if service.ci.detect() == service.ci.CIProvider.GITHUB:
|
511
|
-
click.echo(
|
512
|
-
"Optionally, for reporting results as PR comments, install the Schemathesis GitHub App:\n\n"
|
513
|
-
f" {GITHUB_APP_LINK}"
|
514
|
-
)
|
515
499
|
|
516
500
|
|
517
501
|
def handle_service_integration(context: ServiceReportContext) -> None:
|
schemathesis/constants.py
CHANGED
@@ -13,7 +13,6 @@ USER_AGENT = f"schemathesis/{SCHEMATHESIS_VERSION}"
|
|
13
13
|
SCHEMATHESIS_TEST_CASE_HEADER = "X-Schemathesis-TestCaseId"
|
14
14
|
HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER = ":memory:"
|
15
15
|
DISCORD_LINK = "https://discord.gg/R9ASRAmHnA"
|
16
|
-
GITHUB_APP_LINK = "https://github.com/apps/schemathesis"
|
17
16
|
# Maximum test running time
|
18
17
|
DEFAULT_DEADLINE = 15000
|
19
18
|
DEFAULT_RESPONSE_TIMEOUT = 10000
|
@@ -50,7 +49,6 @@ HOOKS_MODULE_ENV_VAR = "SCHEMATHESIS_HOOKS"
|
|
50
49
|
API_NAME_ENV_VAR = "SCHEMATHESIS_API_NAME"
|
51
50
|
BASE_URL_ENV_VAR = "SCHEMATHESIS_BASE_URL"
|
52
51
|
WAIT_FOR_SCHEMA_ENV_VAR = "SCHEMATHESIS_WAIT_FOR_SCHEMA"
|
53
|
-
REPORT_SUGGESTION_ENV_VAR = "SCHEMATHESIS_REPORT_SUGGESTION"
|
54
52
|
|
55
53
|
TRUE_VALUES = ("y", "yes", "t", "true", "on", "1")
|
56
54
|
FALSE_VALUES = ("n", "no", "f", "false", "off", "0")
|
schemathesis/failures.py
CHANGED
@@ -143,6 +143,12 @@ class AcceptedNegativeData(FailureContext):
|
|
143
143
|
title: str = "Accepted negative data"
|
144
144
|
type: str = "accepted_negative_data"
|
145
145
|
|
146
|
+
def unique_by_key(self, check_message: str | None) -> tuple[str, ...]:
|
147
|
+
return (
|
148
|
+
check_message or self.message,
|
149
|
+
str(self.status_code),
|
150
|
+
)
|
151
|
+
|
146
152
|
|
147
153
|
@dataclass(repr=False)
|
148
154
|
class RejectedPositiveData(FailureContext):
|
schemathesis/internal/checks.py
CHANGED
@@ -21,7 +21,9 @@ CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[b
|
|
21
21
|
@dataclass
|
22
22
|
class NegativeDataRejectionConfig:
|
23
23
|
# 5xx will pass through
|
24
|
-
allowed_statuses: list[str] = field(
|
24
|
+
allowed_statuses: list[str] = field(
|
25
|
+
default_factory=lambda: ["400", "401", "403", "404", "406", "422", "428", "5xx"]
|
26
|
+
)
|
25
27
|
|
26
28
|
|
27
29
|
@dataclass
|
@@ -69,22 +69,6 @@ class RunnerContext:
|
|
69
69
|
def is_stopped(self) -> bool:
|
70
70
|
return self.stop_event.is_set()
|
71
71
|
|
72
|
-
@property
|
73
|
-
def has_all_not_found(self) -> bool:
|
74
|
-
"""Check if all responses are 404."""
|
75
|
-
has_not_found = False
|
76
|
-
for entry in self.data.results:
|
77
|
-
for check in entry.checks:
|
78
|
-
if check.response is not None:
|
79
|
-
if check.response.status_code == 404:
|
80
|
-
has_not_found = True
|
81
|
-
else:
|
82
|
-
# There are non-404 responses, no reason to check any other response
|
83
|
-
return False
|
84
|
-
# Only happens if all responses are 404, or there are no responses at all.
|
85
|
-
# In the first case, it returns True, for the latter - False
|
86
|
-
return has_not_found
|
87
|
-
|
88
72
|
def add_result(self, result: TestResult) -> None:
|
89
73
|
self.data.append(result)
|
90
74
|
|
schemathesis/runner/impl/core.py
CHANGED
@@ -147,6 +147,44 @@ class BaseRunner:
|
|
147
147
|
__probes = None
|
148
148
|
__analysis: Result[AnalysisResult, Exception] | None = None
|
149
149
|
|
150
|
+
def _should_warn_about_only_4xx(result: TestResult) -> bool:
|
151
|
+
if all(check.response is None for check in result.checks):
|
152
|
+
return False
|
153
|
+
# Don't duplicate auth warnings
|
154
|
+
if {check.response.status_code for check in result.checks if check.response is not None} <= {401, 403}:
|
155
|
+
return False
|
156
|
+
# At this point we know we only have 4xx responses
|
157
|
+
return True
|
158
|
+
|
159
|
+
def _check_warnings() -> None:
|
160
|
+
# Warn if all positive test cases got 4xx in return and no failure was found
|
161
|
+
def all_positive_are_rejected(result: TestResult) -> bool:
|
162
|
+
seen_positive = False
|
163
|
+
for check in result.checks:
|
164
|
+
if check.example.data_generation_method != DataGenerationMethod.positive:
|
165
|
+
continue
|
166
|
+
seen_positive = True
|
167
|
+
if check.response is None:
|
168
|
+
continue
|
169
|
+
# At least one positive response for positive test case
|
170
|
+
if 200 <= check.response.status_code < 300:
|
171
|
+
return False
|
172
|
+
# If there are positive test cases, and we ended up here, then there are no 2xx responses for them
|
173
|
+
# Otherwise, there are no positive test cases at all and this check should pass
|
174
|
+
return seen_positive
|
175
|
+
|
176
|
+
for result in ctx.data.results:
|
177
|
+
# Only warn about 4xx responses in successful positive test scenarios
|
178
|
+
if (
|
179
|
+
all(check.value == Status.success for check in result.checks)
|
180
|
+
and DataGenerationMethod.positive in result.data_generation_method
|
181
|
+
and all_positive_are_rejected(result)
|
182
|
+
and _should_warn_about_only_4xx(result)
|
183
|
+
):
|
184
|
+
ctx.add_warning(
|
185
|
+
f"`{result.verbose_name}` returned only 4xx responses during unit tests. Check base URL or adjust data generation settings"
|
186
|
+
)
|
187
|
+
|
150
188
|
def _initialize() -> events.Initialized:
|
151
189
|
nonlocal initialized
|
152
190
|
initialized = events.Initialized.from_schema(
|
@@ -159,8 +197,7 @@ class BaseRunner:
|
|
159
197
|
return initialized
|
160
198
|
|
161
199
|
def _finish() -> events.Finished:
|
162
|
-
|
163
|
-
ctx.add_warning(ALL_NOT_FOUND_WARNING_MESSAGE)
|
200
|
+
_check_warnings()
|
164
201
|
return events.Finished.from_results(results=ctx.data, running_time=time.monotonic() - start_time)
|
165
202
|
|
166
203
|
def _before_probes() -> events.BeforeProbing:
|
@@ -742,9 +779,6 @@ def has_too_many_responses_with_status(result: TestResult, status_code: int) ->
|
|
742
779
|
return unauthorized_count / total >= TOO_MANY_RESPONSES_THRESHOLD
|
743
780
|
|
744
781
|
|
745
|
-
ALL_NOT_FOUND_WARNING_MESSAGE = "All API responses have a 404 status code. Did you specify the proper API location?"
|
746
|
-
|
747
|
-
|
748
782
|
def setup_hypothesis_database_key(test: Callable, operation: APIOperation) -> None:
|
749
783
|
"""Make Hypothesis use separate database entries for every API operation.
|
750
784
|
|
@@ -294,7 +294,12 @@ def missing_required_header(ctx: CheckContext, response: GenericResponse, case:
|
|
294
294
|
|
295
295
|
|
296
296
|
def unsupported_method(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
|
297
|
-
if
|
297
|
+
if (
|
298
|
+
case.meta
|
299
|
+
and case.meta.description
|
300
|
+
and case.meta.description.startswith("Unspecified HTTP method:")
|
301
|
+
and response.request.method != "OPTIONS"
|
302
|
+
):
|
298
303
|
if response.status_code != 405:
|
299
304
|
raise AssertionError(
|
300
305
|
f"Unexpected response status for unspecified HTTP method: {response.status_code}\nExpected: 405"
|
@@ -9,6 +9,8 @@ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
|
|
9
9
|
import requests
|
10
10
|
from hypothesis_jsonschema import from_schema
|
11
11
|
|
12
|
+
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
13
|
+
|
12
14
|
from ...constants import DEFAULT_RESPONSE_TIMEOUT
|
13
15
|
from ...generation import get_single_example
|
14
16
|
from ...internal.copy import fast_deepcopy
|
@@ -48,11 +50,7 @@ def get_strategies_from_examples(
|
|
48
50
|
operation: APIOperation[OpenAPIParameter, Case], as_strategy_kwargs: dict[str, Any] | None = None
|
49
51
|
) -> list[SearchStrategy[Case]]:
|
50
52
|
"""Build a set of strategies that generate test cases based on explicit examples in the schema."""
|
51
|
-
maps =
|
52
|
-
for location, container in LOCATION_TO_CONTAINER.items():
|
53
|
-
serializer = operation.get_parameter_serializer(location)
|
54
|
-
if serializer is not None:
|
55
|
-
maps[container] = serializer
|
53
|
+
maps = get_serializers_for_operation(operation)
|
56
54
|
|
57
55
|
def serialize_components(case: Case) -> Case:
|
58
56
|
"""Applies special serialization rules for case components.
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
3
3
|
import re
|
4
4
|
from functools import lru_cache
|
5
5
|
|
6
|
+
from ...exceptions import InternalError
|
7
|
+
|
6
8
|
try: # pragma: no cover
|
7
9
|
import re._constants as sre
|
8
10
|
import re._parser as sre_parse
|
@@ -29,7 +31,15 @@ def update_quantifier(pattern: str, min_length: int | None, max_length: int | No
|
|
29
31
|
|
30
32
|
try:
|
31
33
|
parsed = sre_parse.parse(pattern)
|
32
|
-
|
34
|
+
updated = _handle_parsed_pattern(parsed, pattern, min_length, max_length)
|
35
|
+
try:
|
36
|
+
re.compile(updated)
|
37
|
+
except re.error as exc:
|
38
|
+
raise InternalError(
|
39
|
+
f"The combination of min_length={min_length} and max_length={max_length} applied to the original pattern '{pattern}' resulted in an invalid regex: '{updated}'. "
|
40
|
+
"This indicates a bug in the regex quantifier merging logic"
|
41
|
+
) from exc
|
42
|
+
return updated
|
33
43
|
except re.error:
|
34
44
|
# Invalid pattern
|
35
45
|
return pattern
|
@@ -261,13 +271,18 @@ def _handle_repeat_quantifier(
|
|
261
271
|
min_length, max_length = _build_size(min_repeat, max_repeat, min_length, max_length)
|
262
272
|
if min_length > max_length:
|
263
273
|
return pattern
|
264
|
-
|
274
|
+
inner = _strip_quantifier(pattern)
|
275
|
+
if inner.startswith("(") and inner.endswith(")"):
|
276
|
+
inner = inner[1:-1]
|
277
|
+
return f"({inner})" + _build_quantifier(min_length, max_length)
|
265
278
|
|
266
279
|
|
267
280
|
def _handle_literal_or_in_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
|
268
281
|
"""Handle literal or character class quantifiers."""
|
269
282
|
min_length = 1 if min_length is None else max(min_length, 1)
|
270
|
-
|
283
|
+
if pattern.startswith("(") and pattern.endswith(")"):
|
284
|
+
pattern = pattern[1:-1]
|
285
|
+
return f"({pattern})" + _build_quantifier(min_length, max_length)
|
271
286
|
|
272
287
|
|
273
288
|
def _build_quantifier(minimum: int | None, maximum: int | None) -> str:
|
@@ -3,6 +3,9 @@ from __future__ import annotations
|
|
3
3
|
import json
|
4
4
|
from typing import Any, Callable, Dict, Generator, List
|
5
5
|
|
6
|
+
from schemathesis.schemas import APIOperation
|
7
|
+
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
8
|
+
|
6
9
|
from ...utils import compose
|
7
10
|
|
8
11
|
Generated = Dict[str, Any]
|
@@ -11,6 +14,15 @@ DefinitionList = List[Definition]
|
|
11
14
|
MapFunction = Callable[[Generated], Generated]
|
12
15
|
|
13
16
|
|
17
|
+
def get_serializers_for_operation(operation: APIOperation) -> dict[str, Callable]:
|
18
|
+
serializers = {}
|
19
|
+
for location, container in LOCATION_TO_CONTAINER.items():
|
20
|
+
serializer = operation.get_parameter_serializer(location)
|
21
|
+
if serializer is not None:
|
22
|
+
serializers[container] = serializer
|
23
|
+
return serializers
|
24
|
+
|
25
|
+
|
14
26
|
def make_serializer(
|
15
27
|
func: Callable[[DefinitionList], Generator[Callable | None, None, None]],
|
16
28
|
) -> Callable[[DefinitionList], Callable | None]:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: schemathesis
|
3
|
-
Version: 3.39.
|
3
|
+
Version: 3.39.11
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
6
6
|
Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
|
@@ -1,7 +1,7 @@
|
|
1
1
|
schemathesis/__init__.py,sha256=UW2Bq8hDDkcBeAAA7PzpBFXkOOxkmHox-mfQwzHDjL0,1914
|
2
2
|
schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
|
3
3
|
schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
|
4
|
-
schemathesis/_hypothesis.py,sha256=
|
4
|
+
schemathesis/_hypothesis.py,sha256=2v6nQk5wiV0z6M_JPbOYhXJTXJ3Cyf6oWfef5VYCNdQ,30350
|
5
5
|
schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
|
6
6
|
schemathesis/_override.py,sha256=TAjYB3eJQmlw9K_xiR9ptt9Wj7if4U7UFlUhGjpBAoM,1625
|
7
7
|
schemathesis/_patches.py,sha256=Hsbpn4UVeXUQD2Kllrbq01CSWsTYENWa0VJTyhX5C2k,895
|
@@ -10,9 +10,9 @@ schemathesis/_xml.py,sha256=qc2LydEwIqcSfgqQOJqiYicivA4YFJGKgCBOem_JqNc,8560
|
|
10
10
|
schemathesis/auths.py,sha256=De97IS_iOlC36-jRhkZ2DUndjUpXYgsd8R-nA-iHn88,16837
|
11
11
|
schemathesis/checks.py,sha256=YPUI1N5giGBy1072vd77e6HWelGAKrJUmJLEG4oqfF8,2630
|
12
12
|
schemathesis/code_samples.py,sha256=rsdTo6ksyUs3ZMhqx0mmmkPSKUCFa--snIOYsXgZd80,4120
|
13
|
-
schemathesis/constants.py,sha256=
|
13
|
+
schemathesis/constants.py,sha256=RHwog2lAz84qG6KCpP1U15A4a9w1xcwbgZ97aY4juQg,2555
|
14
14
|
schemathesis/exceptions.py,sha256=5zjPlyVoQNJGbwufplL6ZVV7FEBPBNPHGdlQRJ7xnhE,20449
|
15
|
-
schemathesis/failures.py,sha256=
|
15
|
+
schemathesis/failures.py,sha256=OsQM5FDs79usUM4MLA20WIDaoW6SKTR39IyKDjeiwtQ,8197
|
16
16
|
schemathesis/filters.py,sha256=f3c_yXIBwIin-9Y0qU2TkcC1NEM_Mw34jGUHQc0BOyw,17026
|
17
17
|
schemathesis/graphql.py,sha256=XiuKcfoOB92iLFC8zpz2msLkM0_V0TLdxPNBqrrGZ8w,216
|
18
18
|
schemathesis/hooks.py,sha256=p5AXgjVGtka0jn9MOeyBaRUtNbqZTs4iaJqytYTacHc,14856
|
@@ -41,7 +41,7 @@ schemathesis/cli/options.py,sha256=yL7nrzKkbGCc4nQya9wpTW48XGz_OT9hOFrzPxRrDe4,2
|
|
41
41
|
schemathesis/cli/reporting.py,sha256=KC3sxSc1u4aFQ-0Q8CQ3G4HTEl7QxlubGnJgNKmVJdQ,3627
|
42
42
|
schemathesis/cli/sanitization.py,sha256=Onw_NWZSom6XTVNJ5NHnC0PAhrYAcGzIXJbsBCzLkn4,1005
|
43
43
|
schemathesis/cli/output/__init__.py,sha256=AXaUzQ1nhQ-vXhW4-X-91vE2VQtEcCOrGtQXXNN55iQ,29
|
44
|
-
schemathesis/cli/output/default.py,sha256=
|
44
|
+
schemathesis/cli/output/default.py,sha256=kRJPcZ5RL9Nsy9k4bSZaRtAezzPiHE6hybdNZLfrEhs,39071
|
45
45
|
schemathesis/cli/output/short.py,sha256=CL6-Apxr5tuZ3BL1vecV1MiRY1wDt21g0wiUwZu6mLM,2607
|
46
46
|
schemathesis/contrib/__init__.py,sha256=FH8NL8NXgSKBFOF8Jy_EB6T4CJEaiM-tmDhz16B2o4k,187
|
47
47
|
schemathesis/contrib/unique_data.py,sha256=cTjJfoNpfLMobUzmGnm3k6kVrZcL34_FMPLlpDDsg4c,1249
|
@@ -63,7 +63,7 @@ schemathesis/generation/_hypothesis.py,sha256=74fzLPHugZgMQXerWYFAMqCAjtAXz5E4ge
|
|
63
63
|
schemathesis/generation/_methods.py,sha256=r8oVlJ71_gXcnEhU-byw2E0R2RswQQFm8U7yGErSqbw,1204
|
64
64
|
schemathesis/generation/coverage.py,sha256=1CilQSe2DIdMdeWA6RL22so2bZULPRwc0CQBRxcLRFs,39370
|
65
65
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
66
|
-
schemathesis/internal/checks.py,sha256=
|
66
|
+
schemathesis/internal/checks.py,sha256=ZPvsPJ7gWwK0IpzBFgOMaq4L2e0yfeC8qxPrnpauVFA,2741
|
67
67
|
schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
|
68
68
|
schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
|
69
69
|
schemathesis/internal/deprecation.py,sha256=XnzwSegbbdQyoTF1OGW_s9pdjIfN_Uzzdb2rfah1w2o,1261
|
@@ -79,8 +79,8 @@ schemathesis/runner/events.py,sha256=cRKKSDvHvKLBIyFBz-J0JtAKshbGGKco9eaMyLCgzsY
|
|
79
79
|
schemathesis/runner/probes.py,sha256=no5AfO3kse25qvHevjeUfB0Q3C860V2AYzschUW3QMQ,5688
|
80
80
|
schemathesis/runner/serialization.py,sha256=vZi1wd9HX9Swp9VJ_hZFeDgy3Y726URpHra-TbPvQhk,20762
|
81
81
|
schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
|
82
|
-
schemathesis/runner/impl/context.py,sha256=
|
83
|
-
schemathesis/runner/impl/core.py,sha256=
|
82
|
+
schemathesis/runner/impl/context.py,sha256=KY06FXVOFQ6DBaa_FomSBXL81ULs3D21IW1u3yLqs1E,2434
|
83
|
+
schemathesis/runner/impl/core.py,sha256=pB_0zs_MfzAcbrVvRU85mONAn8QXVqFfM_y7yV8a2l0,49962
|
84
84
|
schemathesis/runner/impl/solo.py,sha256=y5QSxgK8nBCEjZVD5BpFvYUXmB6tEjk6TwxAo__NejA,2911
|
85
85
|
schemathesis/runner/impl/threadpool.py,sha256=yNR5LYE8f3N_4t42OwSgy0_qdGgBPM7d11F9c9oEAAs,15075
|
86
86
|
schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
|
@@ -107,21 +107,21 @@ schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzM
|
|
107
107
|
schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
|
108
108
|
schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
|
109
109
|
schemathesis/specs/openapi/_hypothesis.py,sha256=nU8UDn1PzGCre4IVmwIuO9-CZv1KJe1fYY0d2BojhSo,22981
|
110
|
-
schemathesis/specs/openapi/checks.py,sha256=
|
110
|
+
schemathesis/specs/openapi/checks.py,sha256=cuHTZsoHV2fdUz23_F99-mLelT1xtvaiS9EcG7R-hZs,26897
|
111
111
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
112
112
|
schemathesis/specs/openapi/converter.py,sha256=Yxw9lS_JKEyi-oJuACT07fm04bqQDlAu-iHwzkeDvE4,3546
|
113
113
|
schemathesis/specs/openapi/definitions.py,sha256=WTkWwCgTc3OMxfKsqh6YDoGfZMTThSYrHGp8h0vLAK0,93935
|
114
|
-
schemathesis/specs/openapi/examples.py,sha256=
|
114
|
+
schemathesis/specs/openapi/examples.py,sha256=yBK0hjq5ROjk7BCLe7BO2dr7raijeZ6_KlZEol-cU-E,20401
|
115
115
|
schemathesis/specs/openapi/formats.py,sha256=3KtEC-8nQRwMErS-WpMadXsr8R0O-NzYwFisZqMuc-8,2761
|
116
116
|
schemathesis/specs/openapi/links.py,sha256=C4Uir2P_EcpqME8ee_a1vdUM8Tm3ZcKNn2YsGjZiMUQ,17935
|
117
117
|
schemathesis/specs/openapi/loaders.py,sha256=jlTYLoG5sVRh8xycIF2M2VDCZ44M80Sct07a_ycg1Po,25698
|
118
118
|
schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
|
119
119
|
schemathesis/specs/openapi/parameters.py,sha256=X_3PKqUScIiN_vbSFEauPYyxASyFv-_9lZ_9QEZRLqo,14655
|
120
|
-
schemathesis/specs/openapi/patterns.py,sha256=
|
120
|
+
schemathesis/specs/openapi/patterns.py,sha256=1hhhLJTJtF2snYEAEd_RzAwEBrNB5ayCXff--Fv6JEs,11881
|
121
121
|
schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
|
122
122
|
schemathesis/specs/openapi/schemas.py,sha256=JA9SiBnwYg75kYnd4_0CWOuQv_XTfYwuDeGmFe4RtVo,53724
|
123
123
|
schemathesis/specs/openapi/security.py,sha256=Z-6pk2Ga1PTUtBe298KunjVHsNh5A-teegeso7zcPIE,7138
|
124
|
-
schemathesis/specs/openapi/serialization.py,sha256=
|
124
|
+
schemathesis/specs/openapi/serialization.py,sha256=rcZfqQbWer_RELedu4Sh5h_RhKYPWTfUjnmLwpP2R_A,11842
|
125
125
|
schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
|
126
126
|
schemathesis/specs/openapi/validation.py,sha256=Q9ThZlwU-mSz7ExDnIivnZGi1ivC5hlX2mIMRAM79kc,999
|
127
127
|
schemathesis/specs/openapi/expressions/__init__.py,sha256=hFpJrIWbPi55GcIVjNFRDDUL8xmDu3mdbdldoHBoFJ0,1729
|
@@ -153,8 +153,8 @@ schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZ
|
|
153
153
|
schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
|
154
154
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
155
155
|
schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
|
156
|
-
schemathesis-3.39.
|
157
|
-
schemathesis-3.39.
|
158
|
-
schemathesis-3.39.
|
159
|
-
schemathesis-3.39.
|
160
|
-
schemathesis-3.39.
|
156
|
+
schemathesis-3.39.11.dist-info/METADATA,sha256=iM14usTIHG9JMQNJsUkBWUUQEcuYJwL74itZ9mZmvXc,11977
|
157
|
+
schemathesis-3.39.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
158
|
+
schemathesis-3.39.11.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
159
|
+
schemathesis-3.39.11.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
160
|
+
schemathesis-3.39.11.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|