schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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 +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Iterator, Mapping, cast
|
|
5
|
+
|
|
6
|
+
from schemathesis.core.errors import InfiniteRecursiveReference
|
|
7
|
+
from schemathesis.core.jsonschema.bundler import BundleError
|
|
8
|
+
from schemathesis.core.jsonschema.types import get_type
|
|
9
|
+
from schemathesis.specs.openapi.adapter.parameters import resource_name_from_ref
|
|
10
|
+
from schemathesis.specs.openapi.adapter.references import maybe_resolve
|
|
11
|
+
from schemathesis.specs.openapi.stateful.dependencies import naming
|
|
12
|
+
from schemathesis.specs.openapi.stateful.dependencies.models import (
|
|
13
|
+
CanonicalizationCache,
|
|
14
|
+
Cardinality,
|
|
15
|
+
DefinitionSource,
|
|
16
|
+
OperationMap,
|
|
17
|
+
ResourceDefinition,
|
|
18
|
+
ResourceMap,
|
|
19
|
+
extend_pointer,
|
|
20
|
+
)
|
|
21
|
+
from schemathesis.specs.openapi.stateful.dependencies.naming import from_path
|
|
22
|
+
from schemathesis.specs.openapi.stateful.dependencies.schemas import (
|
|
23
|
+
ROOT_POINTER,
|
|
24
|
+
canonicalize,
|
|
25
|
+
try_unwrap_composition,
|
|
26
|
+
unwrap_schema,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from schemathesis.core.compat import RefResolver
|
|
31
|
+
from schemathesis.schemas import APIOperation
|
|
32
|
+
from schemathesis.specs.openapi.adapter.responses import OpenApiResponse
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ExtractedResource:
|
|
37
|
+
"""How a resource was extracted from a response."""
|
|
38
|
+
|
|
39
|
+
resource: ResourceDefinition
|
|
40
|
+
# Where in response body (JSON pointer)
|
|
41
|
+
pointer: str
|
|
42
|
+
# Is this a single resource or an array?
|
|
43
|
+
cardinality: Cardinality
|
|
44
|
+
|
|
45
|
+
__slots__ = ("resource", "pointer", "cardinality")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def extract_resources_from_responses(
|
|
49
|
+
*,
|
|
50
|
+
operation: APIOperation,
|
|
51
|
+
resources: ResourceMap,
|
|
52
|
+
updated_resources: set[str],
|
|
53
|
+
resolver: RefResolver,
|
|
54
|
+
canonicalization_cache: CanonicalizationCache,
|
|
55
|
+
) -> Iterator[tuple[OpenApiResponse, ExtractedResource]]:
|
|
56
|
+
"""Extract resource definitions from operation's successful responses.
|
|
57
|
+
|
|
58
|
+
Processes each 2xx response, unwrapping pagination wrappers,
|
|
59
|
+
handling `allOf` / `oneOf` / `anyOf` composition, and determining cardinality.
|
|
60
|
+
Updates the global resource registry as resources are discovered.
|
|
61
|
+
"""
|
|
62
|
+
for response in operation.responses.iter_successful_responses():
|
|
63
|
+
for extracted in iter_resources_from_response(
|
|
64
|
+
path=operation.path,
|
|
65
|
+
response=response,
|
|
66
|
+
resources=resources,
|
|
67
|
+
updated_resources=updated_resources,
|
|
68
|
+
resolver=resolver,
|
|
69
|
+
canonicalization_cache=canonicalization_cache,
|
|
70
|
+
):
|
|
71
|
+
yield response, extracted
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def iter_resources_from_response(
|
|
75
|
+
*,
|
|
76
|
+
path: str,
|
|
77
|
+
response: OpenApiResponse,
|
|
78
|
+
resources: ResourceMap,
|
|
79
|
+
updated_resources: set[str],
|
|
80
|
+
resolver: RefResolver,
|
|
81
|
+
canonicalization_cache: CanonicalizationCache,
|
|
82
|
+
) -> Iterator[ExtractedResource]:
|
|
83
|
+
schema = response.get_raw_schema()
|
|
84
|
+
|
|
85
|
+
if isinstance(schema, bool):
|
|
86
|
+
boolean_resource = _resource_from_boolean_schema(path=path, resources=resources)
|
|
87
|
+
if boolean_resource is not None:
|
|
88
|
+
yield boolean_resource
|
|
89
|
+
return None
|
|
90
|
+
elif not isinstance(schema, dict):
|
|
91
|
+
# Ignore invalid schemas
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
parent_ref = schema.get("$ref")
|
|
95
|
+
_, resolved = maybe_resolve(schema, resolver, "")
|
|
96
|
+
|
|
97
|
+
# Sometimes data is wrapped in a single wrapper field
|
|
98
|
+
# Common patterns: {data: {...}}, {result: {...}}, {response: {...}}
|
|
99
|
+
pointer = None
|
|
100
|
+
properties = resolved.get("properties", {})
|
|
101
|
+
if properties and len(properties) == 1:
|
|
102
|
+
wrapper_field = list(properties)[0]
|
|
103
|
+
# Check if it's a known wrapper field name
|
|
104
|
+
common_wrappers = {"data", "result", "response", "payload"}
|
|
105
|
+
if wrapper_field.lower() in common_wrappers:
|
|
106
|
+
pointer = f"/{wrapper_field}"
|
|
107
|
+
resolved = properties[wrapper_field]
|
|
108
|
+
|
|
109
|
+
resolved = try_unwrap_composition(resolved, resolver)
|
|
110
|
+
|
|
111
|
+
if "allOf" in resolved:
|
|
112
|
+
if parent_ref is not None and parent_ref in canonicalization_cache:
|
|
113
|
+
canonicalized = canonicalization_cache[parent_ref]
|
|
114
|
+
else:
|
|
115
|
+
try:
|
|
116
|
+
canonicalized = canonicalize(cast(dict, resolved), resolver)
|
|
117
|
+
except (InfiniteRecursiveReference, BundleError):
|
|
118
|
+
canonicalized = resolved
|
|
119
|
+
if parent_ref is not None:
|
|
120
|
+
canonicalization_cache[parent_ref] = canonicalized
|
|
121
|
+
else:
|
|
122
|
+
canonicalized = resolved
|
|
123
|
+
|
|
124
|
+
# Detect wrapper pattern and navigate to data
|
|
125
|
+
unwrapped = unwrap_schema(schema=canonicalized, path=path, parent_ref=parent_ref, resolver=resolver)
|
|
126
|
+
|
|
127
|
+
# Recover $ref lost during allOf canonicalization
|
|
128
|
+
recovered_ref = None
|
|
129
|
+
if unwrapped.pointer != ROOT_POINTER and "allOf" in resolved:
|
|
130
|
+
recovered_ref = _recover_ref_from_allof(
|
|
131
|
+
branches=resolved["allOf"],
|
|
132
|
+
pointer=unwrapped.pointer,
|
|
133
|
+
resolver=resolver,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Extract resource and determine cardinality
|
|
137
|
+
result = _extract_resource_and_cardinality(
|
|
138
|
+
schema=unwrapped.schema,
|
|
139
|
+
path=path,
|
|
140
|
+
resources=resources,
|
|
141
|
+
updated_resources=updated_resources,
|
|
142
|
+
resolver=resolver,
|
|
143
|
+
parent_ref=recovered_ref or unwrapped.ref or parent_ref,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if result is not None:
|
|
147
|
+
resource, cardinality = result
|
|
148
|
+
if pointer:
|
|
149
|
+
if unwrapped.pointer != ROOT_POINTER:
|
|
150
|
+
pointer += unwrapped.pointer
|
|
151
|
+
else:
|
|
152
|
+
pointer = unwrapped.pointer
|
|
153
|
+
yield ExtractedResource(resource=resource, cardinality=cardinality, pointer=pointer)
|
|
154
|
+
# Look for sub-resources
|
|
155
|
+
properties = unwrapped.schema.get("properties")
|
|
156
|
+
if isinstance(properties, dict):
|
|
157
|
+
for field, subschema in properties.items():
|
|
158
|
+
if isinstance(subschema, dict):
|
|
159
|
+
reference = subschema.get("$ref")
|
|
160
|
+
if isinstance(reference, str):
|
|
161
|
+
result = _extract_resource_and_cardinality(
|
|
162
|
+
schema=subschema,
|
|
163
|
+
path=path,
|
|
164
|
+
resources=resources,
|
|
165
|
+
updated_resources=updated_resources,
|
|
166
|
+
resolver=resolver,
|
|
167
|
+
parent_ref=reference,
|
|
168
|
+
)
|
|
169
|
+
if result is not None:
|
|
170
|
+
subresource, cardinality = result
|
|
171
|
+
subresource_pointer = extend_pointer(pointer, field, cardinality=cardinality)
|
|
172
|
+
yield ExtractedResource(
|
|
173
|
+
resource=subresource, cardinality=cardinality, pointer=subresource_pointer
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _recover_ref_from_allof(*, branches: list[dict], pointer: str, resolver: RefResolver) -> str | None:
|
|
178
|
+
"""Recover original $ref from allOf branches after canonicalization.
|
|
179
|
+
|
|
180
|
+
Canonicalization inlines all $refs, losing resource name information.
|
|
181
|
+
This searches original allOf branches to find which one defined the
|
|
182
|
+
property at the given pointer.
|
|
183
|
+
"""
|
|
184
|
+
# Parse pointer segments (e.g., "/data" -> ["data"])
|
|
185
|
+
segments = [s for s in pointer.strip("/").split("/") if s]
|
|
186
|
+
|
|
187
|
+
# Search each branch for the property
|
|
188
|
+
for branch in branches:
|
|
189
|
+
_, resolved_branch = maybe_resolve(branch, resolver, "")
|
|
190
|
+
properties = resolved_branch.get("properties", {})
|
|
191
|
+
|
|
192
|
+
# Check if this branch defines the target property
|
|
193
|
+
if segments[-1] in properties:
|
|
194
|
+
# Navigate to property in original (unresolved) branch
|
|
195
|
+
original_properties = branch.get("properties", {})
|
|
196
|
+
if segments[-1] in original_properties:
|
|
197
|
+
prop_schema = original_properties[segments[-1]]
|
|
198
|
+
# Extract $ref from property or its items
|
|
199
|
+
return prop_schema.get("$ref") or prop_schema.get("items", {}).get("$ref")
|
|
200
|
+
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _resource_from_boolean_schema(*, path: str, resources: ResourceMap) -> ExtractedResource | None:
|
|
205
|
+
name = from_path(path)
|
|
206
|
+
if name is None:
|
|
207
|
+
return None
|
|
208
|
+
resource = resources.get(name)
|
|
209
|
+
if resource is None:
|
|
210
|
+
resource = ResourceDefinition.without_properties(name)
|
|
211
|
+
resources[name] = resource
|
|
212
|
+
# Do not update existing resource as if it is inferred, it will have at least one field
|
|
213
|
+
return ExtractedResource(resource=resource, cardinality=Cardinality.ONE, pointer=ROOT_POINTER)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _extract_resource_and_cardinality(
|
|
217
|
+
*,
|
|
218
|
+
schema: Mapping[str, Any],
|
|
219
|
+
path: str,
|
|
220
|
+
resources: ResourceMap,
|
|
221
|
+
updated_resources: set[str],
|
|
222
|
+
resolver: RefResolver,
|
|
223
|
+
parent_ref: str | None = None,
|
|
224
|
+
) -> tuple[ResourceDefinition, Cardinality] | None:
|
|
225
|
+
"""Extract resource from schema and determine cardinality."""
|
|
226
|
+
# Check if it's an array
|
|
227
|
+
if schema.get("type") == "array" or "items" in schema:
|
|
228
|
+
items = schema.get("items")
|
|
229
|
+
if not isinstance(items, dict):
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
# Resolve items if it's a $ref
|
|
233
|
+
_, resolved_items = maybe_resolve(items, resolver, "")
|
|
234
|
+
|
|
235
|
+
# Extract resource from items
|
|
236
|
+
resource = _extract_resource_from_schema(
|
|
237
|
+
schema=resolved_items,
|
|
238
|
+
path=path,
|
|
239
|
+
resources=resources,
|
|
240
|
+
updated_resources=updated_resources,
|
|
241
|
+
resolver=resolver,
|
|
242
|
+
# Prefer items $ref for name
|
|
243
|
+
parent_ref=items.get("$ref") or parent_ref,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if resource is None:
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
return resource, Cardinality.MANY
|
|
250
|
+
|
|
251
|
+
# Single object
|
|
252
|
+
resource = _extract_resource_from_schema(
|
|
253
|
+
schema=schema,
|
|
254
|
+
path=path,
|
|
255
|
+
resources=resources,
|
|
256
|
+
updated_resources=updated_resources,
|
|
257
|
+
resolver=resolver,
|
|
258
|
+
parent_ref=parent_ref,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if resource is None:
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
return resource, Cardinality.ONE
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _extract_resource_from_schema(
|
|
268
|
+
*,
|
|
269
|
+
schema: Mapping[str, Any],
|
|
270
|
+
path: str,
|
|
271
|
+
resources: ResourceMap,
|
|
272
|
+
updated_resources: set[str],
|
|
273
|
+
resolver: RefResolver,
|
|
274
|
+
parent_ref: str | None = None,
|
|
275
|
+
) -> ResourceDefinition | None:
|
|
276
|
+
"""Extract resource definition from a schema."""
|
|
277
|
+
resource_name: str | None = None
|
|
278
|
+
|
|
279
|
+
ref = schema.get("$ref")
|
|
280
|
+
if ref is not None:
|
|
281
|
+
resource_name = resource_name_from_ref(ref)
|
|
282
|
+
elif parent_ref is not None:
|
|
283
|
+
resource_name = resource_name_from_ref(parent_ref)
|
|
284
|
+
else:
|
|
285
|
+
resource_name = naming.from_path(path)
|
|
286
|
+
|
|
287
|
+
if resource_name is None:
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
resource = resources.get(resource_name)
|
|
291
|
+
|
|
292
|
+
if resource is None or resource.source < DefinitionSource.SCHEMA_WITH_PROPERTIES:
|
|
293
|
+
_, resolved = maybe_resolve(schema, resolver, "")
|
|
294
|
+
|
|
295
|
+
if "type" in resolved and resolved["type"] != "object" and "properties" not in resolved:
|
|
296
|
+
# Skip strings, etc
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
properties = resolved.get("properties")
|
|
300
|
+
if properties:
|
|
301
|
+
fields = sorted(properties)
|
|
302
|
+
types = {}
|
|
303
|
+
for field, subschema in properties.items():
|
|
304
|
+
if isinstance(subschema, dict):
|
|
305
|
+
_, resolved_subschema = maybe_resolve(subschema, resolver, "")
|
|
306
|
+
else:
|
|
307
|
+
resolved_subschema = subschema
|
|
308
|
+
types[field] = set(get_type(cast(dict, resolved_subschema)))
|
|
309
|
+
source = DefinitionSource.SCHEMA_WITH_PROPERTIES
|
|
310
|
+
else:
|
|
311
|
+
fields = []
|
|
312
|
+
types = {}
|
|
313
|
+
source = DefinitionSource.SCHEMA_WITHOUT_PROPERTIES
|
|
314
|
+
if resource is not None:
|
|
315
|
+
if resource.source < source:
|
|
316
|
+
resource.source = source
|
|
317
|
+
resource.fields = fields
|
|
318
|
+
resource.types = types
|
|
319
|
+
updated_resources.add(resource_name)
|
|
320
|
+
else:
|
|
321
|
+
resource = ResourceDefinition(name=resource_name, fields=fields, types=types, source=source)
|
|
322
|
+
resources[resource_name] = resource
|
|
323
|
+
|
|
324
|
+
return resource
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def remove_unused_resources(operations: OperationMap, resources: ResourceMap) -> None:
|
|
328
|
+
"""Remove resources that aren't referenced by any operation."""
|
|
329
|
+
# Collect all resource names currently in use
|
|
330
|
+
used_resources = set()
|
|
331
|
+
for operation in operations.values():
|
|
332
|
+
for input_slot in operation.inputs:
|
|
333
|
+
used_resources.add(input_slot.resource.name)
|
|
334
|
+
for output_slot in operation.outputs:
|
|
335
|
+
used_resources.add(output_slot.resource.name)
|
|
336
|
+
|
|
337
|
+
unused = set(resources.keys()) - used_resources
|
|
338
|
+
for resource_name in unused:
|
|
339
|
+
del resources[resource_name]
|