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.
Files changed (251) 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 -1219
  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 +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  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 +748 -82
  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 +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  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.15.4.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.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /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
+ )