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,491 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def from_parameter(parameter: str, path: str) -> str | None:
5
+ parameter = parameter.strip()
6
+ lower = parameter.lower()
7
+
8
+ if lower == "id":
9
+ return from_path(path, parameter_name=parameter)
10
+
11
+ # Capital-sensitive
12
+ capital_suffixes = ("Id", "Uuid", "Guid")
13
+ for suffix in capital_suffixes:
14
+ if parameter.endswith(suffix):
15
+ prefix = parameter[: -len(suffix)]
16
+ if len(prefix) >= 2:
17
+ return to_pascal_case(prefix)
18
+
19
+ # Snake_case (case-insensitive is fine here)
20
+ snake_suffixes = ("_guid", "_uuid", "_id", "-guid", "-uuid", "-id")
21
+ for suffix in snake_suffixes:
22
+ if lower.endswith(suffix):
23
+ prefix = parameter[: -len(suffix)]
24
+ if len(prefix) >= 2:
25
+ return to_pascal_case(prefix)
26
+
27
+ # Special cases that need exact match
28
+ # Twilio-style, capital S
29
+ if parameter.endswith("Sid"):
30
+ prefix = parameter[:-3]
31
+ if len(prefix) >= 2:
32
+ return to_pascal_case(prefix)
33
+
34
+ return None
35
+
36
+
37
+ def from_path(path: str, parameter_name: str | None = None) -> str | None:
38
+ """Detect resource name from OpenAPI path."""
39
+ segments = [s for s in path.split("/") if s]
40
+
41
+ if not segments:
42
+ # API Root
43
+ return None
44
+
45
+ # If parameter name provided, find the resource it refers to
46
+ if parameter_name:
47
+ placeholder = f"{{{parameter_name}}}"
48
+ try:
49
+ param_index = segments.index(placeholder)
50
+ if param_index > 0:
51
+ resource_segment = segments[param_index - 1]
52
+ if "{" not in resource_segment:
53
+ singular = to_singular(resource_segment)
54
+ return to_pascal_case(singular)
55
+ except ValueError:
56
+ pass # Parameter not found in path
57
+
58
+ # Fallback to last non-parameter segment
59
+ non_param_segments = [s for s in segments if "{" not in s]
60
+ if non_param_segments:
61
+ singular = to_singular(non_param_segments[-1])
62
+ return to_pascal_case(singular)
63
+
64
+ return None
65
+
66
+
67
+ IRREGULAR_TO_PLURAL = {
68
+ "abuse": "abuses",
69
+ "alias": "aliases",
70
+ "analysis": "analyses",
71
+ "anathema": "anathemata",
72
+ "axe": "axes",
73
+ "base": "bases",
74
+ "bookshelf": "bookshelves",
75
+ "cache": "caches",
76
+ "canvas": "canvases",
77
+ "carve": "carves",
78
+ "case": "cases",
79
+ "cause": "causes",
80
+ "child": "children",
81
+ "course": "courses",
82
+ "criterion": "criteria",
83
+ "database": "databases",
84
+ "defense": "defenses",
85
+ "diagnosis": "diagnoses",
86
+ "die": "dice",
87
+ "dingo": "dingoes",
88
+ "disease": "diseases",
89
+ "dogma": "dogmata",
90
+ "dose": "doses",
91
+ "eave": "eaves",
92
+ "echo": "echoes",
93
+ "enterprise": "enterprises",
94
+ "ephemeris": "ephemerides",
95
+ "excuse": "excuses",
96
+ "expense": "expenses",
97
+ "foot": "feet",
98
+ "franchise": "franchises",
99
+ "genus": "genera",
100
+ "goose": "geese",
101
+ "groove": "grooves",
102
+ "half": "halves",
103
+ "horse": "horses",
104
+ "house": "houses",
105
+ "human": "humans",
106
+ "hypothesis": "hypotheses",
107
+ "index": "indices",
108
+ "knife": "knives",
109
+ "lemma": "lemmata",
110
+ "license": "licenses",
111
+ "life": "lives",
112
+ "loaf": "loaves",
113
+ "looey": "looies",
114
+ "man": "men",
115
+ "matrix": "matrices",
116
+ "mouse": "mice",
117
+ "movie": "movies",
118
+ "nose": "noses",
119
+ "oasis": "oases",
120
+ "ox": "oxen",
121
+ "passerby": "passersby",
122
+ "pause": "pauses",
123
+ "person": "people",
124
+ "phase": "phases",
125
+ "phenomenon": "phenomena",
126
+ "pickaxe": "pickaxes",
127
+ "proof": "proofs",
128
+ "purchase": "purchases",
129
+ "purpose": "purposes",
130
+ "quiz": "quizzes",
131
+ "radius": "radii",
132
+ "release": "releases",
133
+ "response": "responses",
134
+ "reuse": "reuses",
135
+ "rose": "roses",
136
+ "scarf": "scarves",
137
+ "self": "selves",
138
+ "sense": "senses",
139
+ "shelf": "shelves",
140
+ "size": "sizes",
141
+ "snooze": "snoozes",
142
+ "stigma": "stigmata",
143
+ "stoma": "stomata",
144
+ "synopsis": "synopses",
145
+ "tense": "tenses",
146
+ "thief": "thieves",
147
+ "tooth": "teeth",
148
+ "tornado": "tornadoes",
149
+ "torpedo": "torpedoes",
150
+ "use": "uses",
151
+ "valve": "valves",
152
+ "vase": "vases",
153
+ "verse": "verses",
154
+ "viscus": "viscera",
155
+ "volcano": "volcanoes",
156
+ "warehouse": "warehouses",
157
+ "wave": "waves",
158
+ "wife": "wives",
159
+ "wolf": "wolves",
160
+ "woman": "women",
161
+ "yes": "yeses",
162
+ "vie": "vies",
163
+ }
164
+ IRREGULAR_TO_SINGULAR = {v: k for k, v in IRREGULAR_TO_PLURAL.items()}
165
+ UNCOUNTABLE = frozenset(
166
+ [
167
+ "access",
168
+ "address",
169
+ "adulthood",
170
+ "advice",
171
+ "agenda",
172
+ "aid",
173
+ "aircraft",
174
+ "alcohol",
175
+ "alias",
176
+ "ammo",
177
+ "analysis",
178
+ "analytics",
179
+ "anime",
180
+ "anonymous",
181
+ "athletics",
182
+ "audio",
183
+ "bias",
184
+ "bison",
185
+ "blood",
186
+ "bream",
187
+ "buffalo",
188
+ "butter",
189
+ "carp",
190
+ "cash",
191
+ "chaos",
192
+ "chassis",
193
+ "chess",
194
+ "clothing",
195
+ "cod",
196
+ "commerce",
197
+ "compass",
198
+ "consensus",
199
+ "cooperation",
200
+ "corps",
201
+ "data",
202
+ "debris",
203
+ "deer",
204
+ "diabetes",
205
+ "diagnosis",
206
+ "digestion",
207
+ "elk",
208
+ "energy",
209
+ "ephemeris",
210
+ "equipment",
211
+ "eries",
212
+ "excretion",
213
+ "expertise",
214
+ "firmware",
215
+ "fish",
216
+ "flounder",
217
+ "fun",
218
+ "gallows",
219
+ "garbage",
220
+ "graffiti",
221
+ "hardware",
222
+ "headquarters",
223
+ "health",
224
+ "herpes",
225
+ "highjinks",
226
+ "homework",
227
+ "housework",
228
+ "information",
229
+ "jeans",
230
+ "justice",
231
+ "kudos",
232
+ "labour",
233
+ "literature",
234
+ "machinery",
235
+ "mackerel",
236
+ "mail",
237
+ "manga",
238
+ "means",
239
+ "media",
240
+ "metadata",
241
+ "mews",
242
+ "money",
243
+ "moose",
244
+ "mud",
245
+ "music",
246
+ "news",
247
+ "only",
248
+ "personnel",
249
+ "pike",
250
+ "plankton",
251
+ "pliers",
252
+ "police",
253
+ "pollution",
254
+ "premises",
255
+ "progress",
256
+ "prometheus",
257
+ "radius",
258
+ "rain",
259
+ "research",
260
+ "rice",
261
+ "salmon",
262
+ "scissors",
263
+ "series",
264
+ "sewage",
265
+ "shambles",
266
+ "sheep",
267
+ "shrimp",
268
+ "software",
269
+ "species",
270
+ "staff",
271
+ "swine",
272
+ "synopsis",
273
+ "tennis",
274
+ "traffic",
275
+ "transportation",
276
+ "trout",
277
+ "tuna",
278
+ "wealth",
279
+ "welfare",
280
+ "whiting",
281
+ "wildebeest",
282
+ "wildlife",
283
+ "wireless",
284
+ "you",
285
+ ]
286
+ )
287
+
288
+
289
+ def _is_word_like(s: str) -> bool:
290
+ """Check if string looks like a word (not a path, technical term, etc)."""
291
+ # Skip empty or very short
292
+ if not s or len(s) < 2:
293
+ return False
294
+ # Skip if contains non-word characters (except underscore and hyphen)
295
+ if not all(c.isalpha() or c in ("_", "-") for c in s):
296
+ return False
297
+ # Skip if has numbers
298
+ return not any(c.isdigit() for c in s)
299
+
300
+
301
+ def to_singular(word: str) -> str:
302
+ if not _is_word_like(word):
303
+ return word
304
+ if word.lower() in UNCOUNTABLE:
305
+ return word
306
+ known_lower = IRREGULAR_TO_SINGULAR.get(word.lower())
307
+ if known_lower is not None:
308
+ # Preserve case: if input was capitalized, capitalize result
309
+ if word[0].isupper():
310
+ return known_lower.capitalize()
311
+ return known_lower
312
+ if word.endswith(("ss", "us")):
313
+ return word
314
+ if word.endswith("ies") and len(word) > 3 and word[-4] not in "aeiou":
315
+ return word[:-3] + "y"
316
+ if word.endswith("sses"):
317
+ return word[:-2]
318
+ if word.endswith(("xes", "zes", "ches", "shes")):
319
+ return word[:-2]
320
+ # Handle "ses" ending: check if it was "se" + "s" or "s" + "es"
321
+ if word.endswith("ses") and len(word) > 3:
322
+ # "gases" has 's' at position -3, formed from "gas" + "es"
323
+ # "statuses" has 's' at position -3, formed from "status" + "es"
324
+ return word[:-2]
325
+ if word.endswith("s"):
326
+ return word[:-1]
327
+ return word
328
+
329
+
330
+ def to_plural(word: str) -> str:
331
+ if not _is_word_like(word):
332
+ return word
333
+ if word.lower() in UNCOUNTABLE:
334
+ return word
335
+ known = IRREGULAR_TO_PLURAL.get(word)
336
+ if known is not None:
337
+ return known
338
+ known_lower = IRREGULAR_TO_PLURAL.get(word.lower())
339
+ if known_lower is not None:
340
+ if word[0].isupper():
341
+ return known_lower.capitalize()
342
+ return known_lower
343
+ # Only change y -> ies after consonants (party -> parties, not day -> days)
344
+ if word.endswith("y") and len(word) > 1 and word[-2] not in "aeiou":
345
+ return word[:-1] + "ies"
346
+ # class -> classes
347
+ if word.endswith("ss"):
348
+ return word + "es"
349
+ # words that normally take -es: box -> boxes
350
+ if word.endswith(("s", "x", "z", "ch", "sh")):
351
+ return word + "es"
352
+ # just add 's' (car -> cars)
353
+ return word + "s"
354
+
355
+
356
+ def to_pascal_case(text: str) -> str:
357
+ # snake_case/kebab-case - split and capitalize each word
358
+ if "_" in text or "-" in text:
359
+ parts = text.replace("-", "_").split("_")
360
+ return "".join(word.capitalize() for word in parts if word)
361
+ # camelCase - just uppercase first letter, preserve the rest
362
+ return text[0].upper() + text[1:] if text else text
363
+
364
+
365
+ def to_snake_case(text: str) -> str:
366
+ text = text.replace("-", "_")
367
+ # Insert underscores before uppercase letters
368
+ result = []
369
+ for i, char in enumerate(text):
370
+ # Add underscore before uppercase (except at start)
371
+ if i > 0 and char.isupper():
372
+ result.append("_")
373
+ result.append(char.lower())
374
+ return "".join(result)
375
+
376
+
377
+ def find_matching_field(*, parameter: str, resource: str, fields: list[str]) -> str | None:
378
+ """Find which resource field matches the parameter name."""
379
+ if not fields:
380
+ return None
381
+
382
+ # Exact match
383
+ if parameter in fields:
384
+ return parameter
385
+
386
+ # Normalize for fuzzy matching
387
+ parameter_normalized = _normalize_for_matching(parameter)
388
+ resource_normalized = _normalize_for_matching(resource)
389
+
390
+ # Normalized exact match
391
+ # `brandId` -> `Brand.BrandId`
392
+ for field in fields:
393
+ if _normalize_for_matching(field) == parameter_normalized:
394
+ return field
395
+
396
+ # Extract parameter components
397
+ parameter_prefix, parameter_suffix = _split_parameter_name(parameter)
398
+ parameter_prefix_normalized = _normalize_for_matching(parameter_prefix)
399
+
400
+ # Parameter has resource prefix, field might not
401
+ # Example: `channelId` - `Channel.id`
402
+ if parameter_prefix and parameter_prefix_normalized == resource_normalized:
403
+ suffix_normalized = _normalize_for_matching(parameter_suffix)
404
+
405
+ for field in fields:
406
+ field_normalized = _normalize_for_matching(field)
407
+ if field_normalized == suffix_normalized:
408
+ return field
409
+
410
+ # Parameter has no prefix, field might have resource prefix
411
+ # Example: `id` - `Channel.channelId`
412
+ if not parameter_prefix and parameter_suffix:
413
+ expected_field_normalized = resource_normalized + _normalize_for_matching(parameter_suffix)
414
+
415
+ for field in fields:
416
+ field_normalized = _normalize_for_matching(field)
417
+ if field_normalized == expected_field_normalized:
418
+ return field
419
+
420
+ # ID field synonym matching (for identifier parameters)
421
+ # Match parameter like 'conversation_id' or 'id' with fields like 'uuid', 'guid', 'uid'
422
+ parameter_prefix, parameter_suffix = _split_parameter_name(parameter)
423
+ suffix_normalized = _normalize_for_matching(parameter_suffix)
424
+
425
+ # Common identifier field names in priority order (id, uuid, guid, uid)
426
+ ID_FIELD_NAMES = ["id", "uuid", "guid", "uid"]
427
+
428
+ if suffix_normalized in ID_FIELD_NAMES:
429
+ # Try to match with any identifier field, preferring exact match first
430
+ for id_name in ID_FIELD_NAMES:
431
+ for field in fields:
432
+ if _normalize_for_matching(field) == id_name:
433
+ return field
434
+
435
+ return None
436
+
437
+
438
+ def _normalize_for_matching(text: str) -> str:
439
+ """Normalize text for case-insensitive, separator-insensitive matching.
440
+
441
+ Examples:
442
+ "channelId" -> "channelid"
443
+ "channel_id" -> "channelid"
444
+ "ChannelId" -> "channelid"
445
+ "Channel" -> "channel"
446
+
447
+ """
448
+ return text.lower().replace("_", "").replace("-", "")
449
+
450
+
451
+ def _split_parameter_name(parameter_name: str) -> tuple[str, str]:
452
+ """Split parameter into (prefix, suffix) components.
453
+
454
+ Examples:
455
+ "channelId" -> ("channel", "Id")
456
+ "userId" -> ("user", "Id")
457
+ "user_id" -> ("user", "_id")
458
+ "id" -> ("", "id")
459
+ "channel_id" -> ("channel", "_id")
460
+
461
+ """
462
+ if parameter_name.endswith("Id") and len(parameter_name) > 2:
463
+ return (parameter_name[:-2], "Id")
464
+
465
+ if parameter_name.endswith("_id") and len(parameter_name) > 3:
466
+ return (parameter_name[:-3], "_id")
467
+
468
+ if parameter_name.endswith("_guid") and len(parameter_name) > 5:
469
+ return (parameter_name[:-5], "_guid")
470
+
471
+ return ("", parameter_name)
472
+
473
+
474
+ def strip_affixes(name: str, prefixes: list[str], suffixes: list[str]) -> str:
475
+ """Remove common prefixes and suffixes from a name (case-insensitive)."""
476
+ result = name.strip()
477
+ name_lower = result.lower()
478
+
479
+ # Remove one matching prefix
480
+ for prefix in prefixes:
481
+ if name_lower.startswith(prefix):
482
+ result = result[len(prefix) :]
483
+ break
484
+
485
+ # Remove one matching suffix
486
+ for suffix in suffixes:
487
+ if name_lower.endswith(suffix):
488
+ result = result[: -len(suffix)]
489
+ break
490
+
491
+ return result.strip()
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Iterator
4
+
5
+ from schemathesis.specs.openapi.stateful.dependencies.models import CanonicalizationCache, OutputSlot, ResourceMap
6
+ from schemathesis.specs.openapi.stateful.dependencies.resources import extract_resources_from_responses
7
+
8
+ if TYPE_CHECKING:
9
+ from schemathesis.core.compat import RefResolver
10
+ from schemathesis.specs.openapi.schemas import APIOperation
11
+
12
+
13
+ def extract_outputs(
14
+ *,
15
+ operation: APIOperation,
16
+ resources: ResourceMap,
17
+ updated_resources: set[str],
18
+ resolver: RefResolver,
19
+ canonicalization_cache: CanonicalizationCache,
20
+ ) -> Iterator[OutputSlot]:
21
+ """Extract resources from API operation's responses."""
22
+ for response, extracted in extract_resources_from_responses(
23
+ operation=operation,
24
+ resources=resources,
25
+ updated_resources=updated_resources,
26
+ resolver=resolver,
27
+ canonicalization_cache=canonicalization_cache,
28
+ ):
29
+ yield OutputSlot(
30
+ resource=extracted.resource,
31
+ pointer=extracted.pointer,
32
+ cardinality=extracted.cardinality,
33
+ status_code=response.status_code,
34
+ )