schemathesis 3.25.6__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 -1760
- 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/{runner → engine/phases}/probes.py +50 -67
- 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 +139 -23
- 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 +478 -369
- 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.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.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 -58
- 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 -790
- schemathesis/cli/output/short.py +0 -44
- 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 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- 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 -323
- 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 -199
- 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.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.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),
|
@@ -4,23 +4,26 @@ from contextlib import suppress
|
|
4
4
|
from dataclasses import dataclass
|
5
5
|
from functools import lru_cache
|
6
6
|
from itertools import chain, cycle, islice
|
7
|
-
from typing import TYPE_CHECKING, Any, Generator, Union, cast
|
7
|
+
from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
|
8
8
|
|
9
9
|
import requests
|
10
|
-
from hypothesis.strategies import SearchStrategy
|
11
10
|
from hypothesis_jsonschema import from_schema
|
12
11
|
|
13
|
-
from
|
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
|
19
23
|
from .parameters import OpenAPIBody, OpenAPIParameter
|
20
24
|
|
21
|
-
|
22
25
|
if TYPE_CHECKING:
|
23
|
-
from
|
26
|
+
from hypothesis.strategies import SearchStrategy
|
24
27
|
|
25
28
|
|
26
29
|
@dataclass
|
@@ -44,7 +47,7 @@ Example = Union[ParameterExample, BodyExample]
|
|
44
47
|
|
45
48
|
|
46
49
|
def get_strategies_from_examples(
|
47
|
-
operation: APIOperation[OpenAPIParameter
|
50
|
+
operation: APIOperation[OpenAPIParameter], **kwargs: Any
|
48
51
|
) -> list[SearchStrategy[Case]]:
|
49
52
|
"""Build a set of strategies that generate test cases based on explicit examples in the schema."""
|
50
53
|
maps = {}
|
@@ -68,13 +71,16 @@ def get_strategies_from_examples(
|
|
68
71
|
# Add examples from parameter's schemas
|
69
72
|
examples.extend(extract_from_schemas(operation))
|
70
73
|
return [
|
71
|
-
|
74
|
+
openapi_cases(operation=operation, **{**parameters, **kwargs, "phase": TestPhase.EXPLICIT}).map(
|
75
|
+
serialize_components
|
76
|
+
)
|
72
77
|
for parameters in produce_combinations(examples)
|
73
78
|
]
|
74
79
|
|
75
80
|
|
76
|
-
def extract_top_level(operation: APIOperation[OpenAPIParameter
|
81
|
+
def extract_top_level(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
|
77
82
|
"""Extract top-level parameter examples from `examples` & `example` fields."""
|
83
|
+
responses = find_in_responses(operation)
|
78
84
|
for parameter in operation.iter_parameters():
|
79
85
|
if "schema" in parameter.definition:
|
80
86
|
definitions = [parameter.definition, *_expand_subschemas(parameter.definition["schema"])]
|
@@ -104,6 +110,10 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
|
|
104
110
|
yield ParameterExample(
|
105
111
|
container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
|
106
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
|
+
)
|
107
117
|
for alternative in operation.body:
|
108
118
|
alternative = cast(OpenAPIBody, alternative)
|
109
119
|
if "schema" in alternative.definition:
|
@@ -131,14 +141,30 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
|
|
131
141
|
def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any] | bool, None, None]:
|
132
142
|
yield schema
|
133
143
|
if isinstance(schema, dict):
|
134
|
-
for key in ("anyOf", "oneOf"
|
144
|
+
for key in ("anyOf", "oneOf"):
|
135
145
|
if key in schema:
|
136
146
|
for subschema in schema[key]:
|
137
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
|
138
164
|
|
139
165
|
|
140
166
|
def _find_parameter_examples_definition(
|
141
|
-
operation: APIOperation[OpenAPIParameter
|
167
|
+
operation: APIOperation[OpenAPIParameter], parameter_name: str, field_name: str
|
142
168
|
) -> dict[str, Any]:
|
143
169
|
"""Find the original, unresolved `examples` definition of a parameter."""
|
144
170
|
from .schemas import BaseOpenAPISchema
|
@@ -156,13 +182,13 @@ def _find_parameter_examples_definition(
|
|
156
182
|
|
157
183
|
|
158
184
|
def _find_request_body_examples_definition(
|
159
|
-
operation: APIOperation[OpenAPIParameter
|
185
|
+
operation: APIOperation[OpenAPIParameter], alternative: OpenAPIBody
|
160
186
|
) -> dict[str, Any]:
|
161
187
|
"""Find the original, unresolved `examples` definition of a request body variant."""
|
162
188
|
from .schemas import BaseOpenAPISchema
|
163
189
|
|
164
190
|
schema = cast(BaseOpenAPISchema, operation.schema)
|
165
|
-
if schema.
|
191
|
+
if schema.specification.version == "2.0":
|
166
192
|
raw_schema = schema.raw_schema
|
167
193
|
path_data = raw_schema["paths"][operation.path]
|
168
194
|
parameters = chain(path_data[operation.method].get("parameters", []), path_data.get("parameters", []))
|
@@ -183,7 +209,7 @@ def extract_inner_examples(
|
|
183
209
|
) -> Generator[Any, None, None]:
|
184
210
|
"""Extract exact examples values from the `examples` dictionary."""
|
185
211
|
for name, example in examples.items():
|
186
|
-
if "$ref" in unresolved_definition[name]:
|
212
|
+
if "$ref" in unresolved_definition[name] and "value" not in example and "externalValue" not in example:
|
187
213
|
# The example here is a resolved example and should be yielded as is
|
188
214
|
yield example
|
189
215
|
if isinstance(example, dict):
|
@@ -198,12 +224,12 @@ def extract_inner_examples(
|
|
198
224
|
@lru_cache
|
199
225
|
def load_external_example(url: str) -> bytes:
|
200
226
|
"""Load examples the `externalValue` keyword."""
|
201
|
-
response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT
|
227
|
+
response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT)
|
202
228
|
response.raise_for_status()
|
203
229
|
return response.content
|
204
230
|
|
205
231
|
|
206
|
-
def extract_from_schemas(operation: APIOperation[OpenAPIParameter
|
232
|
+
def extract_from_schemas(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
|
207
233
|
"""Extract examples from parameters' schema definitions."""
|
208
234
|
for parameter in operation.iter_parameters():
|
209
235
|
schema = parameter.as_json_schema(operation)
|
@@ -214,12 +240,13 @@ def extract_from_schemas(operation: APIOperation[OpenAPIParameter, Case]) -> Gen
|
|
214
240
|
for alternative in operation.body:
|
215
241
|
alternative = cast(OpenAPIBody, alternative)
|
216
242
|
schema = alternative.as_json_schema(operation)
|
217
|
-
for
|
218
|
-
|
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)
|
219
246
|
|
220
247
|
|
221
248
|
def extract_from_schema(
|
222
|
-
operation: APIOperation[OpenAPIParameter
|
249
|
+
operation: APIOperation[OpenAPIParameter],
|
223
250
|
schema: dict[str, Any],
|
224
251
|
example_field_name: str,
|
225
252
|
examples_field_name: str,
|
@@ -241,6 +268,8 @@ def extract_from_schema(
|
|
241
268
|
if examples_field_name in subsubschema and isinstance(subsubschema[examples_field_name], list):
|
242
269
|
# These are JSON Schema examples, which is an array of values
|
243
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))
|
244
273
|
if not values:
|
245
274
|
if name in required:
|
246
275
|
# Defer generation to only generate these variants if at least one property has examples
|
@@ -279,7 +308,7 @@ def _generate_single_example(
|
|
279
308
|
allow_x00=generation_config.allow_x00,
|
280
309
|
codec=generation_config.codec,
|
281
310
|
)
|
282
|
-
return
|
311
|
+
return examples.generate_one(strategy)
|
283
312
|
|
284
313
|
|
285
314
|
def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
|
@@ -328,3 +357,90 @@ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> G
|
|
328
357
|
}
|
329
358
|
for container, variants in parameters.items()
|
330
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)
|