schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1766
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{cli → engine/phases}/probes.py +63 -70
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +153 -39
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +483 -367
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -55
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -765
- schemathesis/cli/output/short.py +0 -40
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1231
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -555
- schemathesis/runner/events.py +0 -309
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -986
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -315
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,13 +1,20 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
+
|
2
3
|
from itertools import chain
|
3
4
|
from typing import Any, Callable
|
4
5
|
|
5
|
-
from
|
6
|
-
|
6
|
+
from schemathesis.core.transforms import deepclone, transform
|
7
|
+
|
8
|
+
from .patterns import update_quantifier
|
7
9
|
|
8
10
|
|
9
11
|
def to_json_schema(
|
10
|
-
schema: dict[str, Any],
|
12
|
+
schema: dict[str, Any],
|
13
|
+
*,
|
14
|
+
nullable_name: str,
|
15
|
+
copy: bool = True,
|
16
|
+
is_response_schema: bool = False,
|
17
|
+
update_quantifiers: bool = True,
|
11
18
|
) -> dict[str, Any]:
|
12
19
|
"""Convert Open API parameters to JSON Schema.
|
13
20
|
|
@@ -15,7 +22,7 @@ def to_json_schema(
|
|
15
22
|
See a recursive version below.
|
16
23
|
"""
|
17
24
|
if copy:
|
18
|
-
schema =
|
25
|
+
schema = deepclone(schema)
|
19
26
|
if schema.get(nullable_name) is True:
|
20
27
|
del schema[nullable_name]
|
21
28
|
schema = {"anyOf": [schema, {"type": "null"}]}
|
@@ -23,6 +30,8 @@ def to_json_schema(
|
|
23
30
|
if schema_type == "file":
|
24
31
|
schema["type"] = "string"
|
25
32
|
schema["format"] = "binary"
|
33
|
+
if update_quantifiers:
|
34
|
+
update_pattern_in_schema(schema)
|
26
35
|
if schema_type == "object":
|
27
36
|
if is_response_schema:
|
28
37
|
# Write-only properties should not occur in responses
|
@@ -33,6 +42,18 @@ def to_json_schema(
|
|
33
42
|
return schema
|
34
43
|
|
35
44
|
|
45
|
+
def update_pattern_in_schema(schema: dict[str, Any]) -> None:
|
46
|
+
pattern = schema.get("pattern")
|
47
|
+
min_length = schema.get("minLength")
|
48
|
+
max_length = schema.get("maxLength")
|
49
|
+
if pattern and (min_length or max_length):
|
50
|
+
new_pattern = update_quantifier(pattern, min_length, max_length)
|
51
|
+
if new_pattern != pattern:
|
52
|
+
schema.pop("minLength", None)
|
53
|
+
schema.pop("maxLength", None)
|
54
|
+
schema["pattern"] = new_pattern
|
55
|
+
|
56
|
+
|
36
57
|
def rewrite_properties(schema: dict[str, Any], predicate: Callable[[dict[str, Any]], bool]) -> None:
|
37
58
|
required = schema.get("required", [])
|
38
59
|
forbidden = []
|
@@ -71,6 +92,12 @@ def is_read_only(schema: dict[str, Any] | bool) -> bool:
|
|
71
92
|
|
72
93
|
|
73
94
|
def to_json_schema_recursive(
|
74
|
-
schema: dict[str, Any], nullable_name: str, is_response_schema: bool = False
|
95
|
+
schema: dict[str, Any], nullable_name: str, is_response_schema: bool = False, update_quantifiers: bool = True
|
75
96
|
) -> dict[str, Any]:
|
76
|
-
return
|
97
|
+
return transform(
|
98
|
+
schema,
|
99
|
+
to_json_schema,
|
100
|
+
nullable_name=nullable_name,
|
101
|
+
is_response_schema=is_response_schema,
|
102
|
+
update_quantifiers=update_quantifiers,
|
103
|
+
)
|
@@ -1,8 +1,9 @@
|
|
1
1
|
# These schemas are copied from https://github.com/OAI/OpenAPI-Specification/tree/master/schemas
|
2
2
|
from __future__ import annotations
|
3
|
-
from typing import Any, TYPE_CHECKING
|
4
3
|
|
5
|
-
from
|
4
|
+
from typing import TYPE_CHECKING, Any
|
5
|
+
|
6
|
+
from schemathesis.core.lazy_import import lazy_import
|
6
7
|
|
7
8
|
if TYPE_CHECKING:
|
8
9
|
from jsonschema import Validator
|
@@ -1329,6 +1330,8 @@ OPENAPI_30 = {
|
|
1329
1330
|
},
|
1330
1331
|
},
|
1331
1332
|
}
|
1333
|
+
# Generated from the updated schema.yaml from 0035208, which includes unpublished bugfixes
|
1334
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/0035208611701b4f7f2c959eb99a8725cca41e6e/schemas/v3.1/schema.yaml
|
1332
1335
|
OPENAPI_31 = {
|
1333
1336
|
"$id": "https://spec.openapis.org/oas/3.1/schema/2022-10-07",
|
1334
1337
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
@@ -1344,7 +1347,7 @@ OPENAPI_31 = {
|
|
1344
1347
|
},
|
1345
1348
|
"servers": {"type": "array", "items": {"$ref": "#/$defs/server"}, "default": [{"url": "/"}]},
|
1346
1349
|
"paths": {"$ref": "#/$defs/paths"},
|
1347
|
-
"webhooks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item
|
1350
|
+
"webhooks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item"}},
|
1348
1351
|
"components": {"$ref": "#/$defs/components"},
|
1349
1352
|
"security": {"type": "array", "items": {"$ref": "#/$defs/security-requirement"}},
|
1350
1353
|
"tags": {"type": "array", "items": {"$ref": "#/$defs/tag"}},
|
@@ -1399,7 +1402,7 @@ OPENAPI_31 = {
|
|
1399
1402
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#server-object",
|
1400
1403
|
"type": "object",
|
1401
1404
|
"properties": {
|
1402
|
-
"url": {"type": "string"
|
1405
|
+
"url": {"type": "string"},
|
1403
1406
|
"description": {"type": "string"},
|
1404
1407
|
"variables": {"type": "object", "additionalProperties": {"$ref": "#/$defs/server-variable"}},
|
1405
1408
|
},
|
@@ -1438,7 +1441,7 @@ OPENAPI_31 = {
|
|
1438
1441
|
},
|
1439
1442
|
"links": {"type": "object", "additionalProperties": {"$ref": "#/$defs/link-or-reference"}},
|
1440
1443
|
"callbacks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/callbacks-or-reference"}},
|
1441
|
-
"pathItems": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item
|
1444
|
+
"pathItems": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item"}},
|
1442
1445
|
},
|
1443
1446
|
"patternProperties": {
|
1444
1447
|
"^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": {
|
@@ -1460,6 +1463,7 @@ OPENAPI_31 = {
|
|
1460
1463
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object",
|
1461
1464
|
"type": "object",
|
1462
1465
|
"properties": {
|
1466
|
+
"$ref": {"type": "string", "format": "uri-reference"},
|
1463
1467
|
"summary": {"type": "string"},
|
1464
1468
|
"description": {"type": "string"},
|
1465
1469
|
"servers": {"type": "array", "items": {"$ref": "#/$defs/server"}},
|
@@ -1476,11 +1480,6 @@ OPENAPI_31 = {
|
|
1476
1480
|
"$ref": "#/$defs/specification-extensions",
|
1477
1481
|
"unevaluatedProperties": False,
|
1478
1482
|
},
|
1479
|
-
"path-item-or-reference": {
|
1480
|
-
"if": {"type": "object", "required": ["$ref"]},
|
1481
|
-
"then": {"$ref": "#/$defs/reference"},
|
1482
|
-
"else": {"$ref": "#/$defs/path-item"},
|
1483
|
-
},
|
1484
1483
|
"operation": {
|
1485
1484
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object",
|
1486
1485
|
"type": "object",
|
@@ -1541,7 +1540,6 @@ OPENAPI_31 = {
|
|
1541
1540
|
"if": {"properties": {"in": {"const": "path"}}, "required": ["in"]},
|
1542
1541
|
"then": {
|
1543
1542
|
"properties": {
|
1544
|
-
"name": {"pattern": "[^/#?]+$"},
|
1545
1543
|
"style": {"default": "simple", "enum": ["matrix", "label", "simple"]},
|
1546
1544
|
"required": {"const": True},
|
1547
1545
|
},
|
@@ -1661,7 +1659,7 @@ OPENAPI_31 = {
|
|
1661
1659
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object",
|
1662
1660
|
"type": "object",
|
1663
1661
|
"$ref": "#/$defs/specification-extensions",
|
1664
|
-
"additionalProperties": {"$ref": "#/$defs/path-item
|
1662
|
+
"additionalProperties": {"$ref": "#/$defs/path-item"},
|
1665
1663
|
},
|
1666
1664
|
"callbacks-or-reference": {
|
1667
1665
|
"if": {"type": "object", "required": ["$ref"]},
|
@@ -1754,7 +1752,6 @@ OPENAPI_31 = {
|
|
1754
1752
|
"summary": {"type": "string"},
|
1755
1753
|
"description": {"type": "string"},
|
1756
1754
|
},
|
1757
|
-
"unevaluatedProperties": False,
|
1758
1755
|
},
|
1759
1756
|
"schema": {
|
1760
1757
|
"$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object",
|
@@ -1906,11 +1903,7 @@ _VALIDATORS = [
|
|
1906
1903
|
"OPENAPI_31_VALIDATOR",
|
1907
1904
|
]
|
1908
1905
|
|
1909
|
-
__all__ = [
|
1910
|
-
"SWAGGER_20",
|
1911
|
-
"OPENAPI_30",
|
1912
|
-
"OPENAPI_31",
|
1913
|
-
] + _VALIDATORS
|
1906
|
+
__all__ = ["SWAGGER_20", "OPENAPI_30", "OPENAPI_31", *_VALIDATORS]
|
1914
1907
|
|
1915
1908
|
_imports = {
|
1916
1909
|
"SWAGGER_20_VALIDATOR": lambda: make_validator(SWAGGER_20),
|
@@ -3,19 +3,27 @@ from __future__ import annotations
|
|
3
3
|
from contextlib import suppress
|
4
4
|
from dataclasses import dataclass
|
5
5
|
from functools import lru_cache
|
6
|
-
from itertools import
|
7
|
-
from typing import Any, Generator, Union, cast
|
6
|
+
from itertools import chain, cycle, islice
|
7
|
+
from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
|
8
8
|
|
9
9
|
import requests
|
10
|
-
import hypothesis
|
11
10
|
from hypothesis_jsonschema import from_schema
|
12
|
-
from hypothesis.strategies import SearchStrategy
|
13
11
|
|
14
|
-
from .
|
15
|
-
from
|
16
|
-
from
|
17
|
-
from .
|
12
|
+
from schemathesis.core.transforms import deepclone
|
13
|
+
from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
|
14
|
+
from schemathesis.generation import GenerationConfig
|
15
|
+
from schemathesis.generation.case import Case
|
16
|
+
from schemathesis.generation.hypothesis import examples
|
17
|
+
from schemathesis.generation.meta import TestPhase
|
18
|
+
from schemathesis.schemas import APIOperation
|
19
|
+
|
20
|
+
from ._hypothesis import get_default_format_strategies, openapi_cases
|
18
21
|
from .constants import LOCATION_TO_CONTAINER
|
22
|
+
from .formats import STRING_FORMATS
|
23
|
+
from .parameters import OpenAPIBody, OpenAPIParameter
|
24
|
+
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
from hypothesis.strategies import SearchStrategy
|
19
27
|
|
20
28
|
|
21
29
|
@dataclass
|
@@ -39,7 +47,7 @@ Example = Union[ParameterExample, BodyExample]
|
|
39
47
|
|
40
48
|
|
41
49
|
def get_strategies_from_examples(
|
42
|
-
operation: APIOperation[OpenAPIParameter
|
50
|
+
operation: APIOperation[OpenAPIParameter], **kwargs: Any
|
43
51
|
) -> list[SearchStrategy[Case]]:
|
44
52
|
"""Build a set of strategies that generate test cases based on explicit examples in the schema."""
|
45
53
|
maps = {}
|
@@ -63,13 +71,16 @@ def get_strategies_from_examples(
|
|
63
71
|
# Add examples from parameter's schemas
|
64
72
|
examples.extend(extract_from_schemas(operation))
|
65
73
|
return [
|
66
|
-
|
74
|
+
openapi_cases(operation=operation, **{**parameters, **kwargs, "phase": TestPhase.EXPLICIT}).map(
|
75
|
+
serialize_components
|
76
|
+
)
|
67
77
|
for parameters in produce_combinations(examples)
|
68
78
|
]
|
69
79
|
|
70
80
|
|
71
|
-
def extract_top_level(operation: APIOperation[OpenAPIParameter
|
81
|
+
def extract_top_level(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
|
72
82
|
"""Extract top-level parameter examples from `examples` & `example` fields."""
|
83
|
+
responses = find_in_responses(operation)
|
73
84
|
for parameter in operation.iter_parameters():
|
74
85
|
if "schema" in parameter.definition:
|
75
86
|
definitions = [parameter.definition, *_expand_subschemas(parameter.definition["schema"])]
|
@@ -99,6 +110,10 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
|
|
99
110
|
yield ParameterExample(
|
100
111
|
container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
|
101
112
|
)
|
113
|
+
for value in find_matching_in_responses(responses, parameter.name):
|
114
|
+
yield ParameterExample(
|
115
|
+
container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
|
116
|
+
)
|
102
117
|
for alternative in operation.body:
|
103
118
|
alternative = cast(OpenAPIBody, alternative)
|
104
119
|
if "schema" in alternative.definition:
|
@@ -126,14 +141,30 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
|
|
126
141
|
def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any] | bool, None, None]:
|
127
142
|
yield schema
|
128
143
|
if isinstance(schema, dict):
|
129
|
-
for key in ("anyOf", "oneOf"
|
144
|
+
for key in ("anyOf", "oneOf"):
|
130
145
|
if key in schema:
|
131
146
|
for subschema in schema[key]:
|
132
147
|
yield subschema
|
148
|
+
if "allOf" in schema:
|
149
|
+
subschema = deepclone(schema["allOf"][0])
|
150
|
+
for sub in schema["allOf"][1:]:
|
151
|
+
if isinstance(sub, dict):
|
152
|
+
for key, value in sub.items():
|
153
|
+
if key == "properties":
|
154
|
+
subschema.setdefault("properties", {}).update(value)
|
155
|
+
elif key == "required":
|
156
|
+
subschema.setdefault("required", []).extend(value)
|
157
|
+
elif key == "examples":
|
158
|
+
subschema.setdefault("examples", []).extend(value)
|
159
|
+
elif key == "example":
|
160
|
+
subschema.setdefault("examples", []).append(value)
|
161
|
+
else:
|
162
|
+
subschema[key] = value
|
163
|
+
yield subschema
|
133
164
|
|
134
165
|
|
135
166
|
def _find_parameter_examples_definition(
|
136
|
-
operation: APIOperation[OpenAPIParameter
|
167
|
+
operation: APIOperation[OpenAPIParameter], parameter_name: str, field_name: str
|
137
168
|
) -> dict[str, Any]:
|
138
169
|
"""Find the original, unresolved `examples` definition of a parameter."""
|
139
170
|
from .schemas import BaseOpenAPISchema
|
@@ -151,13 +182,13 @@ def _find_parameter_examples_definition(
|
|
151
182
|
|
152
183
|
|
153
184
|
def _find_request_body_examples_definition(
|
154
|
-
operation: APIOperation[OpenAPIParameter
|
185
|
+
operation: APIOperation[OpenAPIParameter], alternative: OpenAPIBody
|
155
186
|
) -> dict[str, Any]:
|
156
187
|
"""Find the original, unresolved `examples` definition of a request body variant."""
|
157
188
|
from .schemas import BaseOpenAPISchema
|
158
189
|
|
159
190
|
schema = cast(BaseOpenAPISchema, operation.schema)
|
160
|
-
if schema.
|
191
|
+
if schema.specification.version == "2.0":
|
161
192
|
raw_schema = schema.raw_schema
|
162
193
|
path_data = raw_schema["paths"][operation.path]
|
163
194
|
parameters = chain(path_data[operation.method].get("parameters", []), path_data.get("parameters", []))
|
@@ -178,7 +209,7 @@ def extract_inner_examples(
|
|
178
209
|
) -> Generator[Any, None, None]:
|
179
210
|
"""Extract exact examples values from the `examples` dictionary."""
|
180
211
|
for name, example in examples.items():
|
181
|
-
if "$ref" in unresolved_definition[name]:
|
212
|
+
if "$ref" in unresolved_definition[name] and "value" not in example and "externalValue" not in example:
|
182
213
|
# The example here is a resolved example and should be yielded as is
|
183
214
|
yield example
|
184
215
|
if isinstance(example, dict):
|
@@ -193,12 +224,12 @@ def extract_inner_examples(
|
|
193
224
|
@lru_cache
|
194
225
|
def load_external_example(url: str) -> bytes:
|
195
226
|
"""Load examples the `externalValue` keyword."""
|
196
|
-
response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT
|
227
|
+
response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT)
|
197
228
|
response.raise_for_status()
|
198
229
|
return response.content
|
199
230
|
|
200
231
|
|
201
|
-
def extract_from_schemas(operation: APIOperation[OpenAPIParameter
|
232
|
+
def extract_from_schemas(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
|
202
233
|
"""Extract examples from parameters' schema definitions."""
|
203
234
|
for parameter in operation.iter_parameters():
|
204
235
|
schema = parameter.as_json_schema(operation)
|
@@ -209,12 +240,13 @@ def extract_from_schemas(operation: APIOperation[OpenAPIParameter, Case]) -> Gen
|
|
209
240
|
for alternative in operation.body:
|
210
241
|
alternative = cast(OpenAPIBody, alternative)
|
211
242
|
schema = alternative.as_json_schema(operation)
|
212
|
-
for
|
213
|
-
|
243
|
+
for example_field, examples_field in (("example", "examples"), ("x-example", "x-examples")):
|
244
|
+
for value in extract_from_schema(operation, schema, example_field, examples_field):
|
245
|
+
yield BodyExample(value=value, media_type=alternative.media_type)
|
214
246
|
|
215
247
|
|
216
248
|
def extract_from_schema(
|
217
|
-
operation: APIOperation[OpenAPIParameter
|
249
|
+
operation: APIOperation[OpenAPIParameter],
|
218
250
|
schema: dict[str, Any],
|
219
251
|
example_field_name: str,
|
220
252
|
examples_field_name: str,
|
@@ -236,6 +268,8 @@ def extract_from_schema(
|
|
236
268
|
if examples_field_name in subsubschema and isinstance(subsubschema[examples_field_name], list):
|
237
269
|
# These are JSON Schema examples, which is an array of values
|
238
270
|
values.extend(subsubschema[examples_field_name])
|
271
|
+
# Check nested examples as well
|
272
|
+
values.extend(extract_from_schema(operation, subsubschema, example_field_name, examples_field_name))
|
239
273
|
if not values:
|
240
274
|
if name in required:
|
241
275
|
# Defer generation to only generate these variants if at least one property has examples
|
@@ -248,7 +282,7 @@ def extract_from_schema(
|
|
248
282
|
# Generated by one of `anyOf` or similar sub-schemas
|
249
283
|
continue
|
250
284
|
subschema = operation.schema.prepare_schema(subschema)
|
251
|
-
generated = _generate_single_example(subschema)
|
285
|
+
generated = _generate_single_example(subschema, operation.schema.generation_config)
|
252
286
|
variants[name] = [generated]
|
253
287
|
# Calculate the maximum number of examples any property has
|
254
288
|
total_combos = max(len(examples) for examples in variants.values())
|
@@ -264,24 +298,17 @@ def extract_from_schema(
|
|
264
298
|
yield [value]
|
265
299
|
|
266
300
|
|
267
|
-
def _generate_single_example(
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
phases=(hypothesis.Phase.generate,),
|
277
|
-
suppress_health_check=list(hypothesis.HealthCheck),
|
301
|
+
def _generate_single_example(
|
302
|
+
schema: dict[str, Any],
|
303
|
+
generation_config: GenerationConfig,
|
304
|
+
) -> Any:
|
305
|
+
strategy = from_schema(
|
306
|
+
schema,
|
307
|
+
custom_formats={**get_default_format_strategies(), **STRING_FORMATS},
|
308
|
+
allow_x00=generation_config.allow_x00,
|
309
|
+
codec=generation_config.codec,
|
278
310
|
)
|
279
|
-
|
280
|
-
examples.append(ex)
|
281
|
-
|
282
|
-
example_generating_inner_function()
|
283
|
-
|
284
|
-
return examples[0]
|
311
|
+
return examples.generate_one(strategy)
|
285
312
|
|
286
313
|
|
287
314
|
def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
|
@@ -330,3 +357,90 @@ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> G
|
|
330
357
|
}
|
331
358
|
for container, variants in parameters.items()
|
332
359
|
}
|
360
|
+
|
361
|
+
|
362
|
+
def find_in_responses(operation: APIOperation) -> dict[str, list[dict[str, Any]]]:
|
363
|
+
"""Find schema examples in responses."""
|
364
|
+
output: dict[str, list[dict[str, Any]]] = {}
|
365
|
+
for status_code, response in operation.definition.raw.get("responses", {}).items():
|
366
|
+
if not str(status_code).startswith("2"):
|
367
|
+
# Check only 2xx responses
|
368
|
+
continue
|
369
|
+
if isinstance(response, dict) and "$ref" in response:
|
370
|
+
_, response = operation.schema.resolver.resolve_in_scope(response, operation.definition.scope) # type:ignore[attr-defined]
|
371
|
+
for media_type, definition in response.get("content", {}).items():
|
372
|
+
schema_ref = definition.get("schema", {}).get("$ref")
|
373
|
+
if schema_ref:
|
374
|
+
name = schema_ref.split("/")[-1]
|
375
|
+
else:
|
376
|
+
name = f"{status_code}/{media_type}"
|
377
|
+
for examples_field, example_field in (
|
378
|
+
("examples", "example"),
|
379
|
+
("x-examples", "x-example"),
|
380
|
+
):
|
381
|
+
examples = definition.get(examples_field, {})
|
382
|
+
for example in examples.values():
|
383
|
+
if "value" in example:
|
384
|
+
output.setdefault(name, []).append(example["value"])
|
385
|
+
if example_field in definition:
|
386
|
+
output.setdefault(name, []).append(definition[example_field])
|
387
|
+
return output
|
388
|
+
|
389
|
+
|
390
|
+
NOT_FOUND = object()
|
391
|
+
|
392
|
+
|
393
|
+
def find_matching_in_responses(examples: dict[str, list], param: str) -> Iterator[Any]:
|
394
|
+
"""Find matching parameter examples."""
|
395
|
+
normalized = param.lower()
|
396
|
+
is_id_param = normalized.endswith("id")
|
397
|
+
# Extract values from response examples that match input parameters.
|
398
|
+
# E.g., for `GET /orders/{id}/`, use "id" or "orderId" from `Order` response
|
399
|
+
# as examples for the "id" path parameter.
|
400
|
+
for schema_name, schema_examples in examples.items():
|
401
|
+
for example in schema_examples:
|
402
|
+
if not isinstance(example, dict):
|
403
|
+
continue
|
404
|
+
# Unwrapping example from `{"item": [{...}]}`
|
405
|
+
if isinstance(example, dict) and len(example) == 1 and list(example)[0].lower() == schema_name.lower():
|
406
|
+
inner = list(example.values())[0]
|
407
|
+
if isinstance(inner, list):
|
408
|
+
for sub_example in inner:
|
409
|
+
found = _find_matching_in_responses(sub_example, schema_name, param, normalized, is_id_param)
|
410
|
+
if found is not NOT_FOUND:
|
411
|
+
yield found
|
412
|
+
continue
|
413
|
+
if isinstance(inner, dict):
|
414
|
+
example = inner
|
415
|
+
found = _find_matching_in_responses(example, schema_name, param, normalized, is_id_param)
|
416
|
+
if found is not NOT_FOUND:
|
417
|
+
yield found
|
418
|
+
|
419
|
+
|
420
|
+
def _find_matching_in_responses(
|
421
|
+
example: dict[str, Any], schema_name: str, param: str, normalized: str, is_id_param: bool
|
422
|
+
) -> Any:
|
423
|
+
# Check for exact match
|
424
|
+
if param in example:
|
425
|
+
return example[param]
|
426
|
+
if is_id_param and param[:-2] in example:
|
427
|
+
return example[param[:-2]]
|
428
|
+
|
429
|
+
# Check for case-insensitive match
|
430
|
+
for key in example:
|
431
|
+
if key.lower() == normalized:
|
432
|
+
return example[key]
|
433
|
+
else:
|
434
|
+
# If no match found and it's an ID parameter, try additional checks
|
435
|
+
if is_id_param:
|
436
|
+
# Check for 'id' if parameter is '{something}Id'
|
437
|
+
if "id" in example:
|
438
|
+
return example["id"]
|
439
|
+
# Check for '{schemaName}Id' or '{schemaName}_id'
|
440
|
+
if normalized == "id" or normalized.startswith(schema_name.lower()):
|
441
|
+
for key in (schema_name, schema_name.lower()):
|
442
|
+
for suffix in ("_id", "Id"):
|
443
|
+
with_suffix = f"{key}{suffix}"
|
444
|
+
if with_suffix in example:
|
445
|
+
return example[with_suffix]
|
446
|
+
return NOT_FOUND
|
@@ -2,14 +2,27 @@
|
|
2
2
|
|
3
3
|
https://swagger.io/docs/specification/links/#runtime-expressions
|
4
4
|
"""
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import json
|
5
9
|
from typing import Any
|
6
10
|
|
7
11
|
from . import lexer, nodes, parser
|
8
12
|
from .context import ExpressionContext
|
9
13
|
|
14
|
+
__all__ = [
|
15
|
+
"lexer",
|
16
|
+
"nodes",
|
17
|
+
"parser",
|
18
|
+
"ExpressionContext",
|
19
|
+
]
|
10
20
|
|
11
|
-
|
21
|
+
|
22
|
+
def evaluate(expr: Any, context: ExpressionContext, evaluate_nested: bool = False) -> Any:
|
12
23
|
"""Evaluate runtime expression in context."""
|
24
|
+
if isinstance(expr, (dict, list)) and evaluate_nested:
|
25
|
+
return _evaluate_nested(expr, context)
|
13
26
|
if not isinstance(expr, str):
|
14
27
|
# Can be a non-string constant
|
15
28
|
return expr
|
@@ -17,4 +30,26 @@ def evaluate(expr: Any, context: ExpressionContext) -> str:
|
|
17
30
|
if len(parts) == 1:
|
18
31
|
return parts[0] # keep the return type the same as the internal value type
|
19
32
|
# otherwise, concatenate into a string
|
20
|
-
return "".join(
|
33
|
+
return "".join(str(part) for part in parts if part is not None)
|
34
|
+
|
35
|
+
|
36
|
+
def _evaluate_nested(expr: dict[str, Any] | list, context: ExpressionContext) -> Any:
|
37
|
+
if isinstance(expr, dict):
|
38
|
+
return {
|
39
|
+
_evaluate_object_key(key, context): evaluate(value, context, evaluate_nested=True)
|
40
|
+
for key, value in expr.items()
|
41
|
+
}
|
42
|
+
return [evaluate(item, context, evaluate_nested=True) for item in expr]
|
43
|
+
|
44
|
+
|
45
|
+
def _evaluate_object_key(key: str, context: ExpressionContext) -> Any:
|
46
|
+
evaluated = evaluate(key, context)
|
47
|
+
if isinstance(evaluated, str):
|
48
|
+
return evaluated
|
49
|
+
if isinstance(evaluated, bool):
|
50
|
+
return "true" if evaluated else "false"
|
51
|
+
if isinstance(evaluated, (int, float)):
|
52
|
+
return str(evaluated)
|
53
|
+
if evaluated is None:
|
54
|
+
return "null"
|
55
|
+
return json.dumps(evaluated)
|
@@ -1,16 +1,14 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from dataclasses import dataclass
|
3
|
-
from typing import TYPE_CHECKING
|
4
2
|
|
3
|
+
from dataclasses import dataclass
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
from ....transports.responses import GenericResponse
|
5
|
+
from schemathesis.core.transport import Response
|
6
|
+
from schemathesis.generation.case import Case
|
9
7
|
|
10
8
|
|
11
9
|
@dataclass
|
12
10
|
class ExpressionContext:
|
13
11
|
"""Context in what an expression are evaluated."""
|
14
12
|
|
15
|
-
response:
|
13
|
+
response: Response
|
16
14
|
case: Case
|
@@ -0,0 +1,23 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
from dataclasses import dataclass
|
5
|
+
|
6
|
+
|
7
|
+
@dataclass
|
8
|
+
class Extractor:
|
9
|
+
def extract(self, value: str) -> str | None:
|
10
|
+
raise NotImplementedError
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class RegexExtractor(Extractor):
|
15
|
+
"""Extract value via a regex."""
|
16
|
+
|
17
|
+
value: re.Pattern
|
18
|
+
|
19
|
+
def extract(self, value: str) -> str | None:
|
20
|
+
match = self.value.search(value)
|
21
|
+
if match is None:
|
22
|
+
return None
|
23
|
+
return match.group(1)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
"""Lexical analysis of runtime expressions."""
|
2
|
+
|
2
3
|
from dataclasses import dataclass
|
3
4
|
from enum import Enum, unique
|
4
5
|
from typing import Callable, Generator
|
@@ -19,33 +20,34 @@ class Token:
|
|
19
20
|
"""Lexical token that may occur in a runtime expression."""
|
20
21
|
|
21
22
|
value: str
|
23
|
+
end: int
|
22
24
|
type_: TokenType
|
23
25
|
|
24
26
|
# Helpers for cleaner instantiation
|
25
27
|
|
26
28
|
@classmethod
|
27
|
-
def variable(cls, value: str) -> "Token":
|
28
|
-
return cls(value, TokenType.VARIABLE)
|
29
|
+
def variable(cls, value: str, end: int) -> "Token":
|
30
|
+
return cls(value, end, TokenType.VARIABLE)
|
29
31
|
|
30
32
|
@classmethod
|
31
|
-
def string(cls, value: str) -> "Token":
|
32
|
-
return cls(value, TokenType.STRING)
|
33
|
+
def string(cls, value: str, end: int) -> "Token":
|
34
|
+
return cls(value, end, TokenType.STRING)
|
33
35
|
|
34
36
|
@classmethod
|
35
|
-
def pointer(cls, value: str) -> "Token":
|
36
|
-
return cls(value, TokenType.POINTER)
|
37
|
+
def pointer(cls, value: str, end: int) -> "Token":
|
38
|
+
return cls(value, end, TokenType.POINTER)
|
37
39
|
|
38
40
|
@classmethod
|
39
|
-
def lbracket(cls) -> "Token":
|
40
|
-
return cls("{", TokenType.LBRACKET)
|
41
|
+
def lbracket(cls, end: int) -> "Token":
|
42
|
+
return cls("{", end, TokenType.LBRACKET)
|
41
43
|
|
42
44
|
@classmethod
|
43
|
-
def rbracket(cls) -> "Token":
|
44
|
-
return cls("}", TokenType.RBRACKET)
|
45
|
+
def rbracket(cls, end: int) -> "Token":
|
46
|
+
return cls("}", end, TokenType.RBRACKET)
|
45
47
|
|
46
48
|
@classmethod
|
47
|
-
def dot(cls) -> "Token":
|
48
|
-
return cls(".", TokenType.DOT)
|
49
|
+
def dot(cls, end: int) -> "Token":
|
50
|
+
return cls(".", end, TokenType.DOT)
|
49
51
|
|
50
52
|
# Helpers for simpler type comparison
|
51
53
|
|
@@ -102,15 +104,15 @@ def tokenize(expression: str) -> TokenGenerator:
|
|
102
104
|
if current_symbol() == "$":
|
103
105
|
start = cursor
|
104
106
|
move_until(lambda: is_eol() or current_symbol() in stop_symbols)
|
105
|
-
yield Token.variable(expression[start:cursor])
|
107
|
+
yield Token.variable(expression[start:cursor], cursor - 1)
|
106
108
|
elif current_symbol() == ".":
|
107
|
-
yield Token.dot()
|
109
|
+
yield Token.dot(cursor)
|
108
110
|
move()
|
109
111
|
elif current_symbol() == "{":
|
110
|
-
yield Token.lbracket()
|
112
|
+
yield Token.lbracket(cursor)
|
111
113
|
move()
|
112
114
|
elif current_symbol() == "}":
|
113
|
-
yield Token.rbracket()
|
115
|
+
yield Token.rbracket(cursor)
|
114
116
|
move()
|
115
117
|
elif current_symbol() == "#":
|
116
118
|
start = cursor
|
@@ -125,8 +127,8 @@ def tokenize(expression: str) -> TokenGenerator:
|
|
125
127
|
# `ID_{$response.body#/foo}_{$response.body#/bar}`
|
126
128
|
# Which is much easier if we treat `}` as a closing bracket of an embedded runtime expression
|
127
129
|
move_until(lambda: is_eol() or current_symbol() == "}")
|
128
|
-
yield Token.pointer(expression[start:cursor])
|
130
|
+
yield Token.pointer(expression[start:cursor], cursor - 1)
|
129
131
|
else:
|
130
132
|
start = cursor
|
131
133
|
move_until(lambda: is_eol() or current_symbol() in stop_symbols)
|
132
|
-
yield Token.string(expression[start:cursor])
|
134
|
+
yield Token.string(expression[start:cursor], cursor - 1)
|