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.
- schemathesis/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1016
- 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 +683 -247
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +27 -0
- schemathesis/specs/graphql/scalars.py +86 -0
- schemathesis/specs/graphql/schemas.py +395 -123
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +578 -317
- 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 +753 -74
- 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 +117 -68
- schemathesis/specs/openapi/negative/mutations.py +294 -104
- schemathesis/specs/openapi/negative/utils.py +3 -6
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +648 -650
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +404 -69
- 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.13.0.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.13.0.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -41
- schemathesis/_hypothesis.py +0 -115
- schemathesis/cli/callbacks.py +0 -188
- schemathesis/cli/cassettes.py +0 -253
- schemathesis/cli/context.py +0 -36
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -51
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -508
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -79
- schemathesis/exceptions.py +0 -207
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -216
- schemathesis/failures.py +0 -131
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/lazy.py +0 -227
- schemathesis/models.py +0 -1041
- schemathesis/parameters.py +0 -88
- schemathesis/runner/__init__.py +0 -460
- schemathesis/runner/events.py +0 -240
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -755
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -189
- schemathesis/serializers.py +0 -233
- schemathesis/service/__init__.py +0 -3
- schemathesis/service/client.py +0 -46
- schemathesis/service/constants.py +0 -12
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -39
- schemathesis/service/models.py +0 -7
- schemathesis/service/serialization.py +0 -153
- schemathesis/service/worker.py +0 -40
- 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 -302
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -413
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -349
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -436
- schemathesis-3.13.0.dist-info/METADATA +0 -202
- schemathesis-3.13.0.dist-info/RECORD +0 -91
- schemathesis-3.13.0.dist-info/entry_points.txt +0 -6
- /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
|
+
)
|