schemathesis 3.13.0__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.
Files changed (245) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1016
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +683 -247
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +27 -0
  127. schemathesis/specs/graphql/scalars.py +86 -0
  128. schemathesis/specs/graphql/schemas.py +395 -123
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +578 -317
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +753 -74
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +117 -68
  154. schemathesis/specs/openapi/negative/mutations.py +294 -104
  155. schemathesis/specs/openapi/negative/utils.py +3 -6
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +648 -650
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +404 -69
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -41
  189. schemathesis/_hypothesis.py +0 -115
  190. schemathesis/cli/callbacks.py +0 -188
  191. schemathesis/cli/cassettes.py +0 -253
  192. schemathesis/cli/context.py +0 -36
  193. schemathesis/cli/debug.py +0 -21
  194. schemathesis/cli/handlers.py +0 -11
  195. schemathesis/cli/junitxml.py +0 -41
  196. schemathesis/cli/options.py +0 -51
  197. schemathesis/cli/output/__init__.py +0 -1
  198. schemathesis/cli/output/default.py +0 -508
  199. schemathesis/cli/output/short.py +0 -40
  200. schemathesis/constants.py +0 -79
  201. schemathesis/exceptions.py +0 -207
  202. schemathesis/extra/_aiohttp.py +0 -27
  203. schemathesis/extra/_flask.py +0 -10
  204. schemathesis/extra/_server.py +0 -16
  205. schemathesis/extra/pytest_plugin.py +0 -216
  206. schemathesis/failures.py +0 -131
  207. schemathesis/fixups/__init__.py +0 -29
  208. schemathesis/fixups/fast_api.py +0 -30
  209. schemathesis/lazy.py +0 -227
  210. schemathesis/models.py +0 -1041
  211. schemathesis/parameters.py +0 -88
  212. schemathesis/runner/__init__.py +0 -460
  213. schemathesis/runner/events.py +0 -240
  214. schemathesis/runner/impl/__init__.py +0 -3
  215. schemathesis/runner/impl/core.py +0 -755
  216. schemathesis/runner/impl/solo.py +0 -85
  217. schemathesis/runner/impl/threadpool.py +0 -367
  218. schemathesis/runner/serialization.py +0 -189
  219. schemathesis/serializers.py +0 -233
  220. schemathesis/service/__init__.py +0 -3
  221. schemathesis/service/client.py +0 -46
  222. schemathesis/service/constants.py +0 -12
  223. schemathesis/service/events.py +0 -39
  224. schemathesis/service/handler.py +0 -39
  225. schemathesis/service/models.py +0 -7
  226. schemathesis/service/serialization.py +0 -153
  227. schemathesis/service/worker.py +0 -40
  228. schemathesis/specs/graphql/loaders.py +0 -215
  229. schemathesis/specs/openapi/constants.py +0 -7
  230. schemathesis/specs/openapi/expressions/context.py +0 -12
  231. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  232. schemathesis/specs/openapi/filters.py +0 -44
  233. schemathesis/specs/openapi/links.py +0 -302
  234. schemathesis/specs/openapi/loaders.py +0 -453
  235. schemathesis/specs/openapi/parameters.py +0 -413
  236. schemathesis/specs/openapi/security.py +0 -129
  237. schemathesis/specs/openapi/validation.py +0 -24
  238. schemathesis/stateful.py +0 -349
  239. schemathesis/targets.py +0 -32
  240. schemathesis/types.py +0 -38
  241. schemathesis/utils.py +0 -436
  242. schemathesis-3.13.0.dist-info/METADATA +0 -202
  243. schemathesis-3.13.0.dist-info/RECORD +0 -91
  244. schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
  245. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -0,0 +1,341 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ import enum
5
+ from collections import defaultdict
6
+ from dataclasses import asdict, dataclass
7
+ from typing import Any, Iterator, Mapping
8
+
9
+ from typing_extensions import TypeAlias
10
+
11
+ from schemathesis.core.parameters import ParameterLocation
12
+ from schemathesis.core.transforms import encode_pointer
13
+ from schemathesis.specs.openapi.stateful.links import SCHEMATHESIS_LINK_EXTENSION
14
+
15
+
16
+ @dataclass
17
+ class DependencyGraph:
18
+ """Graph of API operations and their resource dependencies."""
19
+
20
+ operations: OperationMap
21
+ resources: ResourceMap
22
+
23
+ __slots__ = ("operations", "resources")
24
+
25
+ def serialize(self) -> dict[str, Any]:
26
+ serialized = asdict(self)
27
+
28
+ for operation in serialized["operations"].values():
29
+ del operation["method"]
30
+ del operation["path"]
31
+ for input in operation["inputs"]:
32
+ input["resource"] = input["resource"]["name"]
33
+ for output in operation["outputs"]:
34
+ output["resource"] = output["resource"]["name"]
35
+
36
+ for resource in serialized["resources"].values():
37
+ del resource["name"]
38
+ del resource["source"]
39
+
40
+ return serialized
41
+
42
+ def iter_links(self) -> Iterator[ResponseLinks]:
43
+ """Generate OpenAPI Links connecting producer and consumer operations.
44
+
45
+ Creates links from operations that produce resources to operations that
46
+ consume them. For example: `POST /users` (creates `User`) -> `GET /users/{id}`
47
+ (needs `User.id` parameter).
48
+ """
49
+ encoded_paths = {id(op): encode_pointer(op.path) for op in self.operations.values()}
50
+
51
+ # Index consumers by resource
52
+ consumers_by_resource: dict[int, dict[int, tuple[OperationNode, list[InputSlot]]]] = defaultdict(dict)
53
+ for consumer in self.operations.values():
54
+ consumer_id = id(consumer)
55
+ for input_slot in consumer.inputs:
56
+ resource_id = id(input_slot.resource)
57
+ if consumer_id not in consumers_by_resource[resource_id]:
58
+ consumers_by_resource[resource_id][consumer_id] = (consumer, [])
59
+ consumers_by_resource[resource_id][consumer_id][1].append(input_slot)
60
+
61
+ for producer in self.operations.values():
62
+ producer_path = encoded_paths[id(producer)]
63
+ producer_id = id(producer)
64
+
65
+ for output_slot in producer.outputs:
66
+ # Only iterate over consumers that match this resource
67
+ relevant_consumers = consumers_by_resource.get(id(output_slot.resource), {})
68
+
69
+ for consumer_id, (consumer, input_slots) in relevant_consumers.items():
70
+ # Skip self-references
71
+ if consumer_id == producer_id:
72
+ continue
73
+
74
+ consumer_path = encoded_paths[consumer_id]
75
+ links: dict[str, LinkDefinition] = {}
76
+
77
+ for input_slot in input_slots:
78
+ if input_slot.resource_field is not None:
79
+ body_pointer = extend_pointer(
80
+ output_slot.pointer, input_slot.resource_field, output_slot.cardinality
81
+ )
82
+ else:
83
+ # No resource field means use the whole resource
84
+ body_pointer = output_slot.pointer
85
+ link_name = f"{consumer.method.capitalize()}{input_slot.resource.name}"
86
+ parameters = {}
87
+ request_body: dict[str, Any] | list = {}
88
+ # Data is extracted from response body
89
+ if input_slot.parameter_location == ParameterLocation.BODY:
90
+ if isinstance(input_slot.parameter_name, int):
91
+ request_body = [f"$response.body#{body_pointer}"]
92
+ else:
93
+ request_body = {
94
+ input_slot.parameter_name: f"$response.body#{body_pointer}",
95
+ }
96
+ else:
97
+ parameters = {
98
+ f"{input_slot.parameter_location.value}.{input_slot.parameter_name}": f"$response.body#{body_pointer}",
99
+ }
100
+ existing = links.get(link_name)
101
+ if existing is not None:
102
+ existing.parameters.update(parameters)
103
+ if isinstance(existing.request_body, dict) and isinstance(request_body, dict):
104
+ existing.request_body.update(request_body)
105
+ else:
106
+ existing.request_body = request_body
107
+ continue
108
+ links[link_name] = LinkDefinition(
109
+ operation_ref=f"#/paths/{consumer_path}/{consumer.method}",
110
+ parameters=parameters,
111
+ request_body=request_body,
112
+ )
113
+
114
+ if links:
115
+ yield ResponseLinks(
116
+ producer_operation_ref=f"#/paths/{producer_path}/{producer.method}",
117
+ status_code=output_slot.status_code,
118
+ links=links,
119
+ )
120
+
121
+ def assert_fieldless_resources(self, key: str, known: dict[str, frozenset[str]]) -> None: # pragma: no cover
122
+ """Verify all resources have at least one field."""
123
+ # Fieldless resources usually indicate failed schema extraction, which can be caused by a bug
124
+ known_fieldless = known.get(key, frozenset())
125
+
126
+ for name, resource in self.resources.items():
127
+ if not resource.fields and name not in known_fieldless:
128
+ raise AssertionError(f"Resource {name} has no fields")
129
+
130
+ def assert_incorrect_field_mappings(self, key: str, known: dict[str, frozenset[str]]) -> None:
131
+ """Verify all input slots reference valid fields in their resources."""
132
+ known_mismatches = known.get(key, frozenset())
133
+
134
+ for operation in self.operations.values():
135
+ for input in operation.inputs:
136
+ # Skip unreliable definition sources
137
+ if input.resource.source < DefinitionSource.SCHEMA_WITH_PROPERTIES:
138
+ continue
139
+ resource = self.resources[input.resource.name]
140
+ if (
141
+ input.resource_field not in resource.fields
142
+ and resource.name not in known_mismatches
143
+ and input.resource_field is not None
144
+ ): # pragma: no cover
145
+ message = (
146
+ f"Operation '{operation.method.upper()} {operation.path}': "
147
+ f"InputSlot references field '{input.resource_field}' "
148
+ f"not found in resource '{resource.name}'"
149
+ )
150
+ matches = difflib.get_close_matches(input.resource_field, resource.fields, n=1, cutoff=0.6)
151
+ if matches:
152
+ message += f". Closest field - `{matches[0]}`"
153
+ if resource.fields:
154
+ message += f". Available fields - {', '.join(resource.fields)}"
155
+ else:
156
+ message += ". Resource has no fields"
157
+ raise AssertionError(message)
158
+
159
+
160
+ def extend_pointer(base: str, field: str, cardinality: Cardinality) -> str:
161
+ if not base.endswith("/"):
162
+ base += "/"
163
+ if cardinality == Cardinality.MANY:
164
+ # For arrays, reference first element: /data → /data/0
165
+ base += "0/"
166
+ base += encode_pointer(field)
167
+ return base
168
+
169
+
170
+ @dataclass
171
+ class LinkDefinition:
172
+ """OpenAPI Link Object definition.
173
+
174
+ Represents a single link from a producer operation's response to a
175
+ consumer operation's input parameter.
176
+ """
177
+
178
+ operation_ref: str
179
+ """Reference to target operation (e.g., '#/paths/~1users~1{id}/get')"""
180
+
181
+ parameters: dict[str, str]
182
+ """Parameter mappings (e.g., {'path.id': '$response.body#/id'})"""
183
+
184
+ request_body: dict[str, str] | list
185
+ """Request body (e.g., {'path.id': '$response.body#/id'})"""
186
+
187
+ __slots__ = ("operation_ref", "parameters", "request_body")
188
+
189
+ def to_openapi(self) -> dict[str, Any]:
190
+ """Convert to OpenAPI Links format."""
191
+ links: dict[str, Any] = {
192
+ "operationRef": self.operation_ref,
193
+ SCHEMATHESIS_LINK_EXTENSION: {"is_inferred": True},
194
+ }
195
+ if self.parameters:
196
+ links["parameters"] = self.parameters
197
+ if self.request_body:
198
+ links["requestBody"] = self.request_body
199
+ links[SCHEMATHESIS_LINK_EXTENSION]["merge_body"] = True
200
+ return links
201
+
202
+
203
+ @dataclass
204
+ class ResponseLinks:
205
+ """Collection of OpenAPI Links for a producer operation's response.
206
+
207
+ Represents all links from a single response (e.g., POST /users -> 201)
208
+ to consumer operations that can use the produced resource.
209
+
210
+ Example:
211
+ POST /users -> 201 might have links to:
212
+ - GET /users/{id}
213
+ - PATCH /users/{id}
214
+ - DELETE /users/{id}
215
+
216
+ """
217
+
218
+ producer_operation_ref: str
219
+ """Reference to producer operation (e.g., '#/paths/~1users/post')"""
220
+
221
+ status_code: str
222
+ """Response status code (e.g., '201', '200', 'default')"""
223
+
224
+ links: dict[str, LinkDefinition]
225
+ """Named links (e.g., {'GetUserById': LinkDefinition(...)})"""
226
+
227
+ __slots__ = ("producer_operation_ref", "status_code", "links")
228
+
229
+ def to_openapi(self) -> dict[str, Any]:
230
+ """Convert to OpenAPI response links format."""
231
+ return {name: link_def.to_openapi() for name, link_def in self.links.items()}
232
+
233
+
234
+ @dataclass
235
+ class NormalizedLink:
236
+ """Normalized representation of a link."""
237
+
238
+ path: str
239
+ method: str
240
+ parameters: set[str]
241
+ request_body: Any
242
+
243
+ __slots__ = ("path", "method", "parameters", "request_body")
244
+
245
+
246
+ class Cardinality(str, enum.Enum):
247
+ """Whether there is one or many resources in a slot."""
248
+
249
+ ONE = "ONE"
250
+ MANY = "MANY"
251
+
252
+
253
+ @dataclass
254
+ class OperationNode:
255
+ """An API operation with its input/output dependencies."""
256
+
257
+ method: str
258
+ path: str
259
+ # What this operation NEEDS
260
+ inputs: list[InputSlot]
261
+ # What this operation PRODUCES
262
+ outputs: list[OutputSlot]
263
+
264
+ __slots__ = ("method", "path", "inputs", "outputs")
265
+
266
+
267
+ @dataclass
268
+ class InputSlot:
269
+ """A required input for an operation."""
270
+
271
+ # Which resource is needed
272
+ resource: ResourceDefinition
273
+ # Which field from that resource (e.g., "id").
274
+ # None if passing the whole resource
275
+ resource_field: str | None
276
+ # Where it goes in the request (e.g., "userId")
277
+ # Integer means index in an array (only single items are supported)
278
+ parameter_name: str | int
279
+ parameter_location: ParameterLocation
280
+
281
+ __slots__ = ("resource", "resource_field", "parameter_name", "parameter_location")
282
+
283
+
284
+ @dataclass
285
+ class OutputSlot:
286
+ """Describes how to extract a resource from an operation's response."""
287
+
288
+ # Which resource type
289
+ resource: ResourceDefinition
290
+ # Where in response body (JSON pointer)
291
+ pointer: str
292
+ # Is this a single resource or an array?
293
+ cardinality: Cardinality
294
+ # HTTP status code
295
+ status_code: str
296
+
297
+ __slots__ = ("resource", "pointer", "cardinality", "status_code")
298
+
299
+
300
+ @dataclass
301
+ class ResourceDefinition:
302
+ """A minimal description of a resource structure."""
303
+
304
+ name: str
305
+ # A sorted list of resource fields
306
+ fields: list[str]
307
+ # Field types mapping
308
+ types: dict[str, set[str]]
309
+ # How this resource was created
310
+ source: DefinitionSource
311
+
312
+ __slots__ = ("name", "fields", "types", "source")
313
+
314
+ @classmethod
315
+ def without_properties(cls, name: str) -> ResourceDefinition:
316
+ return cls(name=name, fields=[], types={}, source=DefinitionSource.SCHEMA_WITHOUT_PROPERTIES)
317
+
318
+ @classmethod
319
+ def inferred_from_parameter(cls, name: str, parameter_name: str | None) -> ResourceDefinition:
320
+ fields = [parameter_name] if parameter_name is not None else []
321
+ return cls(name=name, fields=fields, types={}, source=DefinitionSource.PARAMETER_INFERENCE)
322
+
323
+
324
+ class DefinitionSource(enum.IntEnum):
325
+ """Quality level of resource information.
326
+
327
+ Lower values are less reliable and should be replaced by higher values.
328
+ Same values should be merged (union of fields).
329
+ """
330
+
331
+ # From spec but no structural information
332
+ SCHEMA_WITHOUT_PROPERTIES = 0
333
+ # Guessed from parameter names (not in spec)
334
+ PARAMETER_INFERENCE = 1
335
+ # From spec with actual field definitions
336
+ SCHEMA_WITH_PROPERTIES = 2
337
+
338
+
339
+ OperationMap: TypeAlias = dict[str, OperationNode]
340
+ ResourceMap: TypeAlias = dict[str, ResourceDefinition]
341
+ CanonicalizationCache: TypeAlias = dict[str, Mapping[str, Any]]