svc-infra 0.1.506__py3-none-any.whl → 0.1.654__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 (202) hide show
  1. svc_infra/apf_payments/README.md +732 -0
  2. svc_infra/apf_payments/alembic.py +11 -0
  3. svc_infra/apf_payments/models.py +339 -0
  4. svc_infra/apf_payments/provider/__init__.py +4 -0
  5. svc_infra/apf_payments/provider/aiydan.py +797 -0
  6. svc_infra/apf_payments/provider/base.py +270 -0
  7. svc_infra/apf_payments/provider/registry.py +31 -0
  8. svc_infra/apf_payments/provider/stripe.py +873 -0
  9. svc_infra/apf_payments/schemas.py +333 -0
  10. svc_infra/apf_payments/service.py +892 -0
  11. svc_infra/apf_payments/settings.py +67 -0
  12. svc_infra/api/fastapi/__init__.py +6 -0
  13. svc_infra/api/fastapi/admin/__init__.py +3 -0
  14. svc_infra/api/fastapi/admin/add.py +231 -0
  15. svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
  16. svc_infra/api/fastapi/apf_payments/router.py +1082 -0
  17. svc_infra/api/fastapi/apf_payments/setup.py +73 -0
  18. svc_infra/api/fastapi/auth/add.py +15 -6
  19. svc_infra/api/fastapi/auth/gaurd.py +67 -5
  20. svc_infra/api/fastapi/auth/mfa/router.py +18 -9
  21. svc_infra/api/fastapi/auth/routers/account.py +3 -2
  22. svc_infra/api/fastapi/auth/routers/apikey_router.py +11 -5
  23. svc_infra/api/fastapi/auth/routers/oauth_router.py +82 -37
  24. svc_infra/api/fastapi/auth/routers/session_router.py +63 -0
  25. svc_infra/api/fastapi/auth/security.py +3 -1
  26. svc_infra/api/fastapi/auth/settings.py +2 -0
  27. svc_infra/api/fastapi/auth/state.py +1 -1
  28. svc_infra/api/fastapi/billing/router.py +64 -0
  29. svc_infra/api/fastapi/billing/setup.py +19 -0
  30. svc_infra/api/fastapi/cache/add.py +9 -5
  31. svc_infra/api/fastapi/db/nosql/mongo/add.py +33 -27
  32. svc_infra/api/fastapi/db/sql/add.py +40 -18
  33. svc_infra/api/fastapi/db/sql/crud_router.py +176 -14
  34. svc_infra/api/fastapi/db/sql/session.py +16 -0
  35. svc_infra/api/fastapi/db/sql/users.py +14 -2
  36. svc_infra/api/fastapi/dependencies/ratelimit.py +116 -0
  37. svc_infra/api/fastapi/docs/add.py +160 -0
  38. svc_infra/api/fastapi/docs/landing.py +1 -1
  39. svc_infra/api/fastapi/docs/scoped.py +254 -0
  40. svc_infra/api/fastapi/dual/dualize.py +38 -33
  41. svc_infra/api/fastapi/dual/router.py +48 -1
  42. svc_infra/api/fastapi/dx.py +3 -3
  43. svc_infra/api/fastapi/http/__init__.py +0 -0
  44. svc_infra/api/fastapi/http/concurrency.py +14 -0
  45. svc_infra/api/fastapi/http/conditional.py +33 -0
  46. svc_infra/api/fastapi/http/deprecation.py +21 -0
  47. svc_infra/api/fastapi/middleware/errors/handlers.py +45 -7
  48. svc_infra/api/fastapi/middleware/graceful_shutdown.py +87 -0
  49. svc_infra/api/fastapi/middleware/idempotency.py +116 -0
  50. svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
  51. svc_infra/api/fastapi/middleware/optimistic_lock.py +37 -0
  52. svc_infra/api/fastapi/middleware/ratelimit.py +119 -0
  53. svc_infra/api/fastapi/middleware/ratelimit_store.py +84 -0
  54. svc_infra/api/fastapi/middleware/request_id.py +23 -0
  55. svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
  56. svc_infra/api/fastapi/middleware/timeout.py +148 -0
  57. svc_infra/api/fastapi/openapi/mutators.py +768 -55
  58. svc_infra/api/fastapi/ops/add.py +73 -0
  59. svc_infra/api/fastapi/pagination.py +363 -0
  60. svc_infra/api/fastapi/paths/auth.py +14 -14
  61. svc_infra/api/fastapi/paths/prefix.py +0 -1
  62. svc_infra/api/fastapi/paths/user.py +1 -1
  63. svc_infra/api/fastapi/routers/ping.py +1 -0
  64. svc_infra/api/fastapi/setup.py +48 -15
  65. svc_infra/api/fastapi/tenancy/add.py +19 -0
  66. svc_infra/api/fastapi/tenancy/context.py +112 -0
  67. svc_infra/api/fastapi/versioned.py +101 -0
  68. svc_infra/app/README.md +5 -5
  69. svc_infra/billing/__init__.py +23 -0
  70. svc_infra/billing/async_service.py +147 -0
  71. svc_infra/billing/jobs.py +230 -0
  72. svc_infra/billing/models.py +131 -0
  73. svc_infra/billing/quotas.py +101 -0
  74. svc_infra/billing/schemas.py +33 -0
  75. svc_infra/billing/service.py +115 -0
  76. svc_infra/bundled_docs/README.md +5 -0
  77. svc_infra/bundled_docs/__init__.py +1 -0
  78. svc_infra/bundled_docs/getting-started.md +6 -0
  79. svc_infra/cache/__init__.py +4 -0
  80. svc_infra/cache/add.py +158 -0
  81. svc_infra/cache/backend.py +5 -2
  82. svc_infra/cache/decorators.py +19 -1
  83. svc_infra/cache/keys.py +24 -4
  84. svc_infra/cli/__init__.py +32 -8
  85. svc_infra/cli/__main__.py +4 -0
  86. svc_infra/cli/cmds/__init__.py +10 -0
  87. svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +4 -3
  88. svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +4 -4
  89. svc_infra/cli/cmds/db/sql/alembic_cmds.py +120 -14
  90. svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
  91. svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +5 -4
  92. svc_infra/cli/cmds/docs/docs_cmds.py +140 -0
  93. svc_infra/cli/cmds/dx/__init__.py +12 -0
  94. svc_infra/cli/cmds/dx/dx_cmds.py +99 -0
  95. svc_infra/cli/cmds/help.py +4 -0
  96. svc_infra/cli/cmds/jobs/__init__.py +1 -0
  97. svc_infra/cli/cmds/jobs/jobs_cmds.py +43 -0
  98. svc_infra/cli/cmds/obs/obs_cmds.py +4 -3
  99. svc_infra/cli/cmds/sdk/__init__.py +0 -0
  100. svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
  101. svc_infra/data/add.py +61 -0
  102. svc_infra/data/backup.py +53 -0
  103. svc_infra/data/erasure.py +45 -0
  104. svc_infra/data/fixtures.py +40 -0
  105. svc_infra/data/retention.py +55 -0
  106. svc_infra/db/inbox.py +67 -0
  107. svc_infra/db/nosql/mongo/README.md +13 -13
  108. svc_infra/db/outbox.py +104 -0
  109. svc_infra/db/sql/apikey.py +1 -1
  110. svc_infra/db/sql/authref.py +61 -0
  111. svc_infra/db/sql/core.py +2 -2
  112. svc_infra/db/sql/repository.py +52 -12
  113. svc_infra/db/sql/resource.py +5 -0
  114. svc_infra/db/sql/scaffold.py +16 -4
  115. svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +1 -1
  116. svc_infra/db/sql/templates/setup/env_async.py.tmpl +199 -76
  117. svc_infra/db/sql/templates/setup/env_sync.py.tmpl +231 -79
  118. svc_infra/db/sql/tenant.py +79 -0
  119. svc_infra/db/sql/utils.py +18 -4
  120. svc_infra/db/sql/versioning.py +14 -0
  121. svc_infra/docs/acceptance-matrix.md +71 -0
  122. svc_infra/docs/acceptance.md +44 -0
  123. svc_infra/docs/admin.md +425 -0
  124. svc_infra/docs/adr/0002-background-jobs-and-scheduling.md +40 -0
  125. svc_infra/docs/adr/0003-webhooks-framework.md +24 -0
  126. svc_infra/docs/adr/0004-tenancy-model.md +42 -0
  127. svc_infra/docs/adr/0005-data-lifecycle.md +86 -0
  128. svc_infra/docs/adr/0006-ops-slos-and-metrics.md +47 -0
  129. svc_infra/docs/adr/0007-docs-and-sdks.md +83 -0
  130. svc_infra/docs/adr/0008-billing-primitives.md +143 -0
  131. svc_infra/docs/adr/0009-acceptance-harness.md +40 -0
  132. svc_infra/docs/adr/0010-timeouts-and-resource-limits.md +54 -0
  133. svc_infra/docs/adr/0011-admin-scope-and-impersonation.md +73 -0
  134. svc_infra/docs/api.md +59 -0
  135. svc_infra/docs/auth.md +11 -0
  136. svc_infra/docs/billing.md +190 -0
  137. svc_infra/docs/cache.md +76 -0
  138. svc_infra/docs/cli.md +74 -0
  139. svc_infra/docs/contributing.md +34 -0
  140. svc_infra/docs/data-lifecycle.md +52 -0
  141. svc_infra/docs/database.md +14 -0
  142. svc_infra/docs/docs-and-sdks.md +62 -0
  143. svc_infra/docs/environment.md +114 -0
  144. svc_infra/docs/getting-started.md +63 -0
  145. svc_infra/docs/idempotency.md +111 -0
  146. svc_infra/docs/jobs.md +67 -0
  147. svc_infra/docs/observability.md +16 -0
  148. svc_infra/docs/ops.md +37 -0
  149. svc_infra/docs/rate-limiting.md +125 -0
  150. svc_infra/docs/repo-review.md +48 -0
  151. svc_infra/docs/security.md +176 -0
  152. svc_infra/docs/tenancy.md +35 -0
  153. svc_infra/docs/timeouts-and-resource-limits.md +147 -0
  154. svc_infra/docs/versioned-integrations.md +146 -0
  155. svc_infra/docs/webhooks.md +112 -0
  156. svc_infra/dx/add.py +63 -0
  157. svc_infra/dx/changelog.py +74 -0
  158. svc_infra/dx/checks.py +67 -0
  159. svc_infra/http/__init__.py +13 -0
  160. svc_infra/http/client.py +72 -0
  161. svc_infra/jobs/builtins/outbox_processor.py +38 -0
  162. svc_infra/jobs/builtins/webhook_delivery.py +90 -0
  163. svc_infra/jobs/easy.py +32 -0
  164. svc_infra/jobs/loader.py +45 -0
  165. svc_infra/jobs/queue.py +81 -0
  166. svc_infra/jobs/redis_queue.py +191 -0
  167. svc_infra/jobs/runner.py +75 -0
  168. svc_infra/jobs/scheduler.py +41 -0
  169. svc_infra/jobs/worker.py +40 -0
  170. svc_infra/mcp/svc_infra_mcp.py +85 -28
  171. svc_infra/obs/README.md +2 -0
  172. svc_infra/obs/add.py +54 -7
  173. svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
  174. svc_infra/obs/metrics/__init__.py +53 -0
  175. svc_infra/obs/metrics.py +52 -0
  176. svc_infra/security/add.py +201 -0
  177. svc_infra/security/audit.py +130 -0
  178. svc_infra/security/audit_service.py +73 -0
  179. svc_infra/security/headers.py +52 -0
  180. svc_infra/security/hibp.py +95 -0
  181. svc_infra/security/jwt_rotation.py +53 -0
  182. svc_infra/security/lockout.py +96 -0
  183. svc_infra/security/models.py +255 -0
  184. svc_infra/security/org_invites.py +128 -0
  185. svc_infra/security/passwords.py +77 -0
  186. svc_infra/security/permissions.py +149 -0
  187. svc_infra/security/session.py +98 -0
  188. svc_infra/security/signed_cookies.py +80 -0
  189. svc_infra/webhooks/__init__.py +16 -0
  190. svc_infra/webhooks/add.py +322 -0
  191. svc_infra/webhooks/fastapi.py +37 -0
  192. svc_infra/webhooks/router.py +55 -0
  193. svc_infra/webhooks/service.py +67 -0
  194. svc_infra/webhooks/signing.py +30 -0
  195. svc_infra-0.1.654.dist-info/METADATA +154 -0
  196. svc_infra-0.1.654.dist-info/RECORD +352 -0
  197. svc_infra/api/fastapi/deps.py +0 -3
  198. svc_infra-0.1.506.dist-info/METADATA +0 -78
  199. svc_infra-0.1.506.dist-info/RECORD +0 -213
  200. /svc_infra/{api/fastapi/schemas → apf_payments}/__init__.py +0 -0
  201. {svc_infra-0.1.506.dist-info → svc_infra-0.1.654.dist-info}/WHEEL +0 -0
  202. {svc_infra-0.1.506.dist-info → svc_infra-0.1.654.dist-info}/entry_points.txt +0 -0
@@ -47,6 +47,218 @@ def conventions_mutator():
47
47
  return m
48
48
 
49
49
 
50
+ def pagination_components_mutator(
51
+ *,
52
+ default_limit: int = 50,
53
+ max_limit: int = 200,
54
+ ) -> callable:
55
+ """
56
+ Adds reusable pagination/filtering parameters & paginated envelope schemas.
57
+ - Cursor: cursor/limit
58
+ - Page: page/page_size
59
+ - Common filters: q, sort, created_[after|before], updated_[after|before]
60
+ - Envelope: PaginatedList<T>
61
+ """
62
+
63
+ def m(schema: dict) -> dict:
64
+ schema = dict(schema)
65
+ comps = schema.setdefault("components", {})
66
+ params = comps.setdefault("parameters", {})
67
+ schemas = comps.setdefault("schemas", {})
68
+
69
+ # ---- Parameters (reusable) ----
70
+ params.setdefault(
71
+ "cursor",
72
+ {
73
+ "name": "cursor",
74
+ "in": "query",
75
+ "required": False,
76
+ "schema": {"type": "string"},
77
+ "description": "Opaque cursor for forward pagination.",
78
+ },
79
+ )
80
+ params.setdefault(
81
+ "limit",
82
+ {
83
+ "name": "limit",
84
+ "in": "query",
85
+ "required": False,
86
+ "schema": {
87
+ "type": "integer",
88
+ "minimum": 1,
89
+ "maximum": max_limit,
90
+ "default": default_limit,
91
+ },
92
+ "description": f"Max items to return (1..{max_limit}).",
93
+ },
94
+ )
95
+ params.setdefault(
96
+ "page",
97
+ {
98
+ "name": "page",
99
+ "in": "query",
100
+ "required": False,
101
+ "schema": {"type": "integer", "minimum": 1, "default": 1},
102
+ "description": "1-based page index (alternative to cursor).",
103
+ },
104
+ )
105
+ params.setdefault(
106
+ "page_size",
107
+ {
108
+ "name": "page_size",
109
+ "in": "query",
110
+ "required": False,
111
+ "schema": {
112
+ "type": "integer",
113
+ "minimum": 1,
114
+ "maximum": max_limit,
115
+ "default": default_limit,
116
+ },
117
+ "description": f"Number of items per page (1..{max_limit}).",
118
+ },
119
+ )
120
+ params.setdefault(
121
+ "q",
122
+ {
123
+ "name": "q",
124
+ "in": "query",
125
+ "required": False,
126
+ "schema": {"type": "string"},
127
+ "description": "Free-text filter query.",
128
+ },
129
+ )
130
+ params.setdefault(
131
+ "sort",
132
+ {
133
+ "name": "sort",
134
+ "in": "query",
135
+ "required": False,
136
+ "schema": {
137
+ "type": "string",
138
+ "examples": ["created_at", "-created_at", "name", "-name"],
139
+ },
140
+ "description": "Sort field, prefix '-' for descending.",
141
+ },
142
+ )
143
+ for fld in ("created", "updated"):
144
+ params.setdefault(
145
+ f"{fld}_after",
146
+ {
147
+ "name": f"{fld}_after",
148
+ "in": "query",
149
+ "required": False,
150
+ "schema": {"type": "string"},
151
+ "description": f"Only items with {fld}_at strictly after this HTTP-date (RFC 9110).",
152
+ },
153
+ )
154
+ params.setdefault(
155
+ f"{fld}_before",
156
+ {
157
+ "name": f"{fld}_before",
158
+ "in": "query",
159
+ "required": False,
160
+ "schema": {"type": "string"},
161
+ "description": f"Only items with {fld}_at strictly before this HTTP-date (RFC 9110).",
162
+ },
163
+ )
164
+
165
+ # ---- Envelope schema (generic pattern) ----
166
+ # This is a non-generic "template" envelope; concrete versions are produced per endpoint, if desired.
167
+ schemas.setdefault(
168
+ "PaginatedList",
169
+ {
170
+ "type": "object",
171
+ "properties": {
172
+ "items": {"type": "array", "items": {"type": "object"}},
173
+ "next_cursor": {
174
+ "type": "string",
175
+ "nullable": True,
176
+ "description": "Opaque cursor for next page (null when no more).",
177
+ },
178
+ "total": {
179
+ "type": "integer",
180
+ "nullable": True,
181
+ "description": "Total items (may be null if not computed).",
182
+ },
183
+ },
184
+ "required": ["items"],
185
+ },
186
+ )
187
+
188
+ return schema
189
+
190
+ return m
191
+
192
+
193
+ def auto_attach_pagination_params_mutator(
194
+ *,
195
+ mode: str = "cursor_or_page",
196
+ attach_filters: bool = True,
197
+ apply_when: str = "array_200",
198
+ flag_disable: str = "x_no_auto_pagination",
199
+ ) -> callable:
200
+ """
201
+ Attaches reusable pagination/filter parameters to GET "listy" operations.
202
+
203
+ - mode:
204
+ "cursor_or_page" -> attach cursor+limit and page+page_size (clients use either)
205
+ "cursor_only" -> attach cursor+limit
206
+ "page_only" -> attach page+page_size
207
+ - apply_when:
208
+ "array_200" -> only when response 200 schema is an array
209
+ "all_get" -> for every GET op
210
+ - Per-op opt-out: set operation's openapi_extra[flag_disable] = True
211
+ """
212
+
213
+ def _should_apply(op: dict) -> bool:
214
+ if op.get(flag_disable) is True:
215
+ return False
216
+ if apply_when == "all_get":
217
+ return True
218
+ # array_200: inspect 200->application/json->schema
219
+ resps = op.get("responses") or {}
220
+ r200 = resps.get("200")
221
+ if not isinstance(r200, dict):
222
+ return False
223
+ content = r200.get("content") or {}
224
+ mt = content.get("application/json")
225
+ if not isinstance(mt, dict):
226
+ return False
227
+ sch = mt.get("schema") or {}
228
+ return isinstance(sch, dict) and sch.get("type") == "array"
229
+
230
+ def m(schema: dict) -> dict:
231
+ schema = dict(schema)
232
+ for _, method, op in _iter_ops(schema):
233
+ if method != "get":
234
+ continue
235
+ if not _should_apply(op):
236
+ continue
237
+
238
+ params = op.setdefault("parameters", [])
239
+
240
+ def _add_ref(name: str):
241
+ params.append({"$ref": f"#/components/parameters/{name}"})
242
+
243
+ if mode in ("cursor_only", "cursor_or_page"):
244
+ _add_ref("cursor")
245
+ _add_ref("limit")
246
+ if mode in ("page_only", "cursor_or_page"):
247
+ _add_ref("page")
248
+ _add_ref("page_size")
249
+ if attach_filters:
250
+ _add_ref("q")
251
+ _add_ref("sort")
252
+ _add_ref("created_after")
253
+ _add_ref("created_before")
254
+ _add_ref("updated_after")
255
+ _add_ref("updated_before")
256
+
257
+ return schema
258
+
259
+ return m
260
+
261
+
50
262
  def normalize_problem_and_examples_mutator():
51
263
  """
52
264
  1) Force components.schemas.Problem.properties.instance.format = "uri-reference".
@@ -155,6 +367,18 @@ def auth_mutator(include_api_key: bool):
155
367
 
156
368
  def _m(schema: dict) -> dict:
157
369
  schema = dict(schema)
370
+
371
+ # Detect if any operation already references APIKeyHeader
372
+ any_op_wants_api_key = False
373
+ for _, _, op in _iter_ops(schema):
374
+ for sec in op.get("security") or []:
375
+ if isinstance(sec, dict) and "APIKeyHeader" in sec:
376
+ any_op_wants_api_key = True
377
+ break
378
+ if any_op_wants_api_key:
379
+ break
380
+
381
+ # Add OAuth2 (always)
158
382
  comps = schema.setdefault("components", {}).setdefault("securitySchemes", {})
159
383
  comps.setdefault(
160
384
  "OAuth2PasswordBearer",
@@ -163,11 +387,21 @@ def auth_mutator(include_api_key: bool):
163
387
  "flows": {"password": {"tokenUrl": auth_login_path, "scopes": {}}},
164
388
  },
165
389
  )
166
- if include_api_key:
390
+
391
+ # Add API key scheme only if:
392
+ # - include_api_key flag is True (from spec/root), OR
393
+ # - at least one operation already references APIKeyHeader.
394
+ if include_api_key or any_op_wants_api_key:
167
395
  comps.setdefault(
168
- "APIKeyHeader", {"type": "apiKey", "name": "X-API-Key", "in": "header"}
396
+ "APIKeyHeader",
397
+ {
398
+ "type": "apiKey",
399
+ "name": "X-API-Key",
400
+ "in": "header",
401
+ },
169
402
  )
170
403
 
404
+ # Normalize operation security (drop only 'SessionCookie')
171
405
  drop = {"SessionCookie"}
172
406
  for _, _, op in _iter_ops(schema):
173
407
  op["security"] = _normalize_security_list(op.get("security"), drop)
@@ -421,7 +655,9 @@ def ensure_request_body_descriptions_mutator(default_template="Request body for
421
655
 
422
656
 
423
657
  def ensure_parameter_metadata_mutator(param_desc_template="{name} parameter."):
424
- """Add missing descriptions; enforce required for path params; ensure schema exists."""
658
+ """Add missing descriptions; enforce required for path params; ensure schema exists.
659
+ NOTE: Never touch $ref parameters here.
660
+ """
425
661
 
426
662
  def m(schema: dict) -> dict:
427
663
  schema = dict(schema)
@@ -432,6 +668,9 @@ def ensure_parameter_metadata_mutator(param_desc_template="{name} parameter."):
432
668
  for p in params:
433
669
  if not isinstance(p, dict):
434
670
  continue
671
+ if "$ref" in p:
672
+ # leave component parameters untouched
673
+ continue
435
674
  name = p.get("name", "")
436
675
  where = p.get("in", "")
437
676
  # description
@@ -451,8 +690,123 @@ def ensure_parameter_metadata_mutator(param_desc_template="{name} parameter."):
451
690
  return m
452
691
 
453
692
 
693
+ def strip_ref_siblings_in_parameters_mutator():
694
+ """Normalize parameters: if an item has $ref, remove all other keys."""
695
+
696
+ def m(schema: dict) -> dict:
697
+ schema = dict(schema)
698
+ for _, _, op in _iter_ops(schema):
699
+ params = op.get("parameters")
700
+ if not isinstance(params, list):
701
+ continue
702
+ for p in params:
703
+ if isinstance(p, dict) and "$ref" in p and len(p) > 1:
704
+ ref = p["$ref"]
705
+ p.clear()
706
+ p["$ref"] = ref
707
+ return schema
708
+
709
+ return m
710
+
711
+
712
+ def dedupe_parameters_mutator():
713
+ """
714
+ Deduplicate operation.parameters by actual (name, in):
715
+ - Prefer **inline** params over $ref so per-op 'required: true' (e.g., Idempotency-Key) wins.
716
+ - Collapse duplicate $refs.
717
+ - If a $ref and an inline share the same (name, in), keep the inline and drop the $ref.
718
+ """
719
+
720
+ def m(schema: dict) -> dict:
721
+ schema = dict(schema)
722
+ comps = schema.get("components") or {}
723
+ comp_params = (comps.get("parameters") or {}).copy()
724
+
725
+ def _resolve_ref(ref: str) -> tuple[str, str] | None:
726
+ # '#/components/parameters/<Key>' -> ('Idempotency-Key','header'), etc.
727
+ try:
728
+ key = ref.rsplit("/", 1)[-1]
729
+ p = comp_params.get(key) or {}
730
+ name = p.get("name")
731
+ where = p.get("in")
732
+ if isinstance(name, str) and isinstance(where, str):
733
+ return (name, where)
734
+ except Exception:
735
+ pass
736
+ return None
737
+
738
+ for _, _, op in _iter_ops(schema):
739
+ params = op.get("parameters")
740
+ if not isinstance(params, list) or not params:
741
+ continue
742
+
743
+ # First pass: collect inline params by (name, in)
744
+ inline_by_key: dict[tuple[str, str], dict] = {}
745
+ for p in params:
746
+ if isinstance(p, dict) and "$ref" not in p:
747
+ name = p.get("name")
748
+ where = p.get("in")
749
+ if isinstance(name, str) and isinstance(where, str):
750
+ # Prefer the first inline; later duplicates ignored
751
+ inline_by_key.setdefault((name, where), p)
752
+
753
+ seen_ref_targets: set[str] = set()
754
+ seen_keys: set[tuple[str, str]] = set()
755
+ result: list[dict] = []
756
+
757
+ for p in params:
758
+ if not isinstance(p, dict):
759
+ continue
760
+
761
+ if "$ref" in p:
762
+ ref = p.get("$ref")
763
+ if not isinstance(ref, str):
764
+ continue
765
+ # de-dup exact same $ref
766
+ if ref in seen_ref_targets:
767
+ continue
768
+ seen_ref_targets.add(ref)
769
+
770
+ tup = _resolve_ref(ref)
771
+ if tup is None:
772
+ # keep unresolved ref (unlikely)
773
+ result.append({"$ref": ref})
774
+ continue
775
+
776
+ # If an inline with same (name, in) exists, prefer inline, skip this $ref
777
+ if tup in inline_by_key:
778
+ continue
779
+
780
+ # Else, keep this $ref if we didn't already keep a param for that key
781
+ if tup in seen_keys:
782
+ continue
783
+ seen_keys.add(tup)
784
+ # Ensure no siblings remain
785
+ result.append({"$ref": ref})
786
+ continue
787
+
788
+ # inline param
789
+ name = p.get("name")
790
+ where = p.get("in")
791
+ if not (isinstance(name, str) and isinstance(where, str)):
792
+ result.append(p)
793
+ continue
794
+ key = (name, where)
795
+ if key in seen_keys:
796
+ # already kept an inline with same (name, in)
797
+ continue
798
+ seen_keys.add(key)
799
+ result.append(p)
800
+
801
+ op["parameters"] = result
802
+
803
+ return schema
804
+
805
+ return m
806
+
807
+
454
808
  def normalize_no_content_204_mutator():
455
- """Ensure 204 responses have description and no content."""
809
+ """Ensure 204 responses have 'No Content' description and no content."""
456
810
 
457
811
  def m(schema: dict) -> dict:
458
812
  schema = dict(schema)
@@ -462,8 +816,9 @@ def normalize_no_content_204_mutator():
462
816
  continue
463
817
  r204 = resps.get("204")
464
818
  if isinstance(r204, dict):
465
- r204.setdefault("description", "No Content")
466
- # many validators prefer no 'content' for 204
819
+ # Always normalize text to the conventional phrasing
820
+ r204["description"] = "No Content"
821
+ # Many validators prefer no 'content' for 204
467
822
  if "content" in r204:
468
823
  r204.pop("content", None)
469
824
  return schema
@@ -471,6 +826,53 @@ def normalize_no_content_204_mutator():
471
826
  return m
472
827
 
473
828
 
829
+ def inject_safe_examples_mutator():
830
+ """
831
+ Inject a couple of specific, schema-safe examples:
832
+ - If a 2xx application/json response uses #/components/schemas/SendEmailCodeOut
833
+ and has no example(s), set {"sent": true, "cooldown_seconds": 60}.
834
+ - For GET/any 2xx on /ping, add {"status": "ok"} if example(s) are missing.
835
+ Never overwrites existing example/examples.
836
+ """
837
+
838
+ def _has_examples(mt_obj: dict) -> bool:
839
+ return isinstance(mt_obj, dict) and ("example" in mt_obj or "examples" in mt_obj)
840
+
841
+ def m(schema: dict) -> dict:
842
+ schema = dict(schema)
843
+ for path, method, op in _iter_ops(schema):
844
+ resps = op.get("responses") or {}
845
+ for code, resp in resps.items():
846
+ if not isinstance(resp, dict):
847
+ continue
848
+ try:
849
+ ic = int(code)
850
+ except Exception:
851
+ continue
852
+ if not (200 <= ic < 300):
853
+ continue
854
+
855
+ content = resp.get("content") or {}
856
+ mt_obj = content.get("application/json")
857
+ if not isinstance(mt_obj, dict) or _has_examples(mt_obj):
858
+ continue
859
+
860
+ # Special-case: SendEmailCodeOut by $ref
861
+ sch = mt_obj.get("schema") or {}
862
+ ref = sch.get("$ref") if isinstance(sch, dict) else None
863
+ if isinstance(ref, str) and ref.endswith("/SendEmailCodeOut"):
864
+ mt_obj["example"] = {"sent": True, "cooldown_seconds": 60}
865
+ continue
866
+
867
+ # Special-case: /ping success body
868
+ if path == "/ping":
869
+ mt_obj["example"] = {"status": "ok"}
870
+
871
+ return schema
872
+
873
+ return m
874
+
875
+
474
876
  def prune_invalid_responses_keys_mutator():
475
877
  """In an operation's responses object, only status codes or 'default' are allowed."""
476
878
 
@@ -664,12 +1066,11 @@ def improve_success_response_descriptions_mutator():
664
1066
 
665
1067
 
666
1068
  def ensure_success_examples_mutator():
667
- """Ensure 2xx application/json responses have an example."""
1069
+ """Ensure 2xx application/json responses have an example (but only when safe)."""
668
1070
 
669
1071
  def m(schema: dict) -> dict:
670
1072
  schema = dict(schema)
671
1073
  for _, _, op in _iter_ops(schema):
672
- summary = op.get("summary") or ""
673
1074
  resps = op.get("responses") or {}
674
1075
  for code, resp in resps.items():
675
1076
  if not isinstance(resp, dict):
@@ -680,29 +1081,133 @@ def ensure_success_examples_mutator():
680
1081
  continue
681
1082
  if not (200 <= ic < 300) or ic == 204:
682
1083
  continue
683
- content = resp.get("content") or {}
684
- mt_obj = content.get("application/json")
685
- if not isinstance(mt_obj, dict):
686
- continue
687
- if "example" in mt_obj or "examples" in mt_obj:
1084
+ mt_obj = (resp.get("content") or {}).get("application/json")
1085
+ if not isinstance(mt_obj, dict) or "example" in mt_obj or "examples" in mt_obj:
688
1086
  continue
689
1087
  sch = mt_obj.get("schema") or {}
690
- # give minimal but deterministic examples
691
- if sch.get("type") == "string":
1088
+
1089
+ # Only set examples for primitives/arrays. Never for object/$ref.
1090
+ t = sch.get("type")
1091
+ if t == "string":
692
1092
  mt_obj["example"] = "example"
693
- elif sch.get("type") == "boolean":
1093
+ elif t == "boolean":
694
1094
  mt_obj["example"] = True
695
- elif sch.get("type") == "integer":
1095
+ elif t == "integer":
696
1096
  mt_obj["example"] = 0
697
- elif sch.get("type") == "array":
1097
+ elif t == "array":
698
1098
  mt_obj["example"] = []
699
- elif sch.get("type") == "object":
700
- # just put {} if no requireds
701
- if not sch.get("required"):
702
- mt_obj["example"] = {}
703
- # fallback
704
- if "example" not in mt_obj:
705
- mt_obj["example"] = {"status": "ok", "summary": summary}
1099
+ # NOTE: no else/fallback for object/$ref
1100
+ return schema
1101
+
1102
+ return m
1103
+
1104
+
1105
+ # --- NEW: attach minimal x-codeSamples for common operations ---
1106
+ def attach_code_samples_mutator():
1107
+ """Attach minimal curl/httpie x-codeSamples for each operation if missing.
1108
+
1109
+ We avoid templating parameters; samples illustrate method and path only.
1110
+ """
1111
+
1112
+ def m(schema: dict) -> dict:
1113
+ schema = dict(schema)
1114
+ servers = schema.get("servers") or [{"url": ""}]
1115
+ base = servers[0].get("url") or ""
1116
+
1117
+ for path, method, op in _iter_ops(schema):
1118
+ # Don't override existing samples
1119
+ if isinstance(op.get("x-codeSamples"), list) and op["x-codeSamples"]:
1120
+ continue
1121
+ url = f"{base}{path}"
1122
+ method_up = method.upper()
1123
+ samples = [
1124
+ {
1125
+ "lang": "bash",
1126
+ "label": "curl",
1127
+ "source": f"curl -X {method_up} '{url}'",
1128
+ },
1129
+ {
1130
+ "lang": "bash",
1131
+ "label": "httpie",
1132
+ "source": f"http {method_up} '{url}'",
1133
+ },
1134
+ ]
1135
+ op["x-codeSamples"] = samples
1136
+ return schema
1137
+
1138
+ return m
1139
+
1140
+
1141
+ # --- NEW: ensure Problem+JSON examples exist for standard error responses ---
1142
+ def ensure_problem_examples_mutator():
1143
+ """Add example objects for 4xx/5xx responses using Problem schema if absent."""
1144
+
1145
+ try:
1146
+ # Internal helper with sensible defaults
1147
+ from .conventions import _problem_example # type: ignore
1148
+ except Exception: # pragma: no cover - fallback
1149
+
1150
+ def _problem_example(**kw): # type: ignore
1151
+ base = {
1152
+ "type": "about:blank",
1153
+ "title": "Error",
1154
+ "status": 500,
1155
+ "detail": "An error occurred.",
1156
+ "instance": "/request/trace",
1157
+ "code": "INTERNAL_ERROR",
1158
+ }
1159
+ base.update(kw)
1160
+ return base
1161
+
1162
+ def m(schema: dict) -> dict:
1163
+ schema = dict(schema)
1164
+ for _, _, op in _iter_ops(schema):
1165
+ resps = op.get("responses") or {}
1166
+ for code, resp in resps.items():
1167
+ if not isinstance(resp, dict):
1168
+ continue
1169
+ try:
1170
+ ic = int(code)
1171
+ except Exception:
1172
+ continue
1173
+ if ic < 400:
1174
+ continue
1175
+ # Do not add content if response is a $ref; avoid creating siblings
1176
+ if "$ref" in resp:
1177
+ continue
1178
+ content = resp.setdefault("content", {})
1179
+ # prefer problem+json but also set application/json if present
1180
+ for mt in ("application/problem+json", "application/json"):
1181
+ mt_obj = content.get(mt)
1182
+ if mt_obj is None:
1183
+ # Create a basic media type referencing Problem schema when appropriate
1184
+ if mt == "application/problem+json":
1185
+ mt_obj = {"schema": {"$ref": "#/components/schemas/Problem"}}
1186
+ content[mt] = mt_obj
1187
+ else:
1188
+ continue
1189
+ if not isinstance(mt_obj, dict):
1190
+ continue
1191
+ if "example" in mt_obj or "examples" in mt_obj:
1192
+ continue
1193
+ mt_obj["example"] = _problem_example(status=ic)
1194
+ return schema
1195
+
1196
+ return m
1197
+
1198
+
1199
+ # --- NEW: attach default tags from first path segment when missing ---
1200
+ def attach_default_tags_mutator():
1201
+ """If an operation has no tags, tag it by its first path segment."""
1202
+
1203
+ def m(schema: dict) -> dict:
1204
+ schema = dict(schema)
1205
+ for path, _method, op in _iter_ops(schema):
1206
+ tags = op.get("tags")
1207
+ if tags:
1208
+ continue
1209
+ seg = path.strip("/").split("/", 1)[0] or "root"
1210
+ op["tags"] = [seg]
706
1211
  return schema
707
1212
 
708
1213
  return m
@@ -722,32 +1227,33 @@ def dedupe_tags_mutator():
722
1227
 
723
1228
  def scrub_invalid_object_examples_mutator():
724
1229
  """
725
- If a media example is an object but schema has required fields and the example
726
- is {} (or not a dict), remove the example so the validator won't fail.
1230
+ Remove media examples that are objects but don't satisfy required keys
1231
+ (or when schema is a $ref/object). Keeps primitives/arrays.
727
1232
  """
728
1233
 
729
- def patch_content(node: dict):
1234
+ def _invalid_object_example(sch: dict, ex: dict) -> bool:
1235
+ if not isinstance(sch, dict) or not isinstance(ex, dict):
1236
+ return False
1237
+ if "$ref" in sch:
1238
+ return True # can't validate here → drop
1239
+ if sch.get("type") == "object" or "properties" in sch or "required" in sch:
1240
+ req = set(sch.get("required") or [])
1241
+ return bool(req) and not req.issubset(ex.keys())
1242
+ return False
1243
+
1244
+ def _patch(node: dict):
730
1245
  content = node.get("content")
731
1246
  if not isinstance(content, dict):
732
1247
  return
733
1248
  for mt_obj in content.values():
734
1249
  if not isinstance(mt_obj, dict):
735
1250
  continue
736
- if "example" not in mt_obj:
737
- continue
738
1251
  sch = mt_obj.get("schema")
739
1252
  ex = mt_obj.get("example")
740
- if not isinstance(sch, dict):
741
- continue
742
- # Follow $ref if you want; simplest is: only act on obvious object cases.
743
- if (
744
- sch.get("type") == "object"
745
- or "properties" in sch
746
- or "required" in sch
747
- or "$ref" in sch
1253
+ if "example" in mt_obj and _invalid_object_example(
1254
+ sch if isinstance(sch, dict) else {}, ex
748
1255
  ):
749
- if not isinstance(ex, dict) or ex == {}:
750
- mt_obj.pop("example", None)
1256
+ mt_obj.pop("example", None)
751
1257
 
752
1258
  def m(schema: dict) -> dict:
753
1259
  schema = dict(schema)
@@ -756,10 +1262,10 @@ def scrub_invalid_object_examples_mutator():
756
1262
  if isinstance(resps, dict):
757
1263
  for resp in resps.values():
758
1264
  if isinstance(resp, dict):
759
- patch_content(resp)
1265
+ _patch(resp)
760
1266
  rb = op.get("requestBody")
761
1267
  if isinstance(rb, dict):
762
- patch_content(rb)
1268
+ _patch(rb)
763
1269
  return schema
764
1270
 
765
1271
  return m
@@ -792,25 +1298,224 @@ def drop_unused_components_mutator_auto(keep_schemas: set[str] | None = None):
792
1298
 
793
1299
  # schemas
794
1300
  sch = comps.get("schemas") or {}
795
- to_drop = [
796
- name
797
- for name in sch.keys()
798
- if name not in keep_schemas and name not in (used.get("schemas") or set())
799
- ]
800
- for name in to_drop:
801
- sch.pop(name, None)
802
-
803
- # responses (optional; usually all are used)
1301
+ for name in list(sch.keys()):
1302
+ if name not in keep_schemas and name not in (used.get("schemas") or set()):
1303
+ sch.pop(name, None)
1304
+
1305
+ # responses
804
1306
  resps = comps.get("responses") or {}
805
- to_drop_r = [name for name in resps.keys() if name not in (used.get("responses") or set())]
806
- for name in to_drop_r:
807
- resps.pop(name, None)
1307
+ for name in list(resps.keys()):
1308
+ if name not in (used.get("responses") or set()):
1309
+ resps.pop(name, None)
1310
+
1311
+ # NEW: headers
1312
+ hdrs = comps.get("headers") or {}
1313
+ for name in list(hdrs.keys()):
1314
+ if name not in (used.get("headers") or set()):
1315
+ hdrs.pop(name, None)
1316
+
1317
+ # (Optional: parameters/requestBodies/etc. same pattern)
808
1318
 
809
1319
  return schema
810
1320
 
811
1321
  return m
812
1322
 
813
1323
 
1324
+ def hardening_components_mutator():
1325
+ def m(schema: dict) -> dict:
1326
+ schema = dict(schema)
1327
+ comps = schema.setdefault("components", {})
1328
+ params = comps.setdefault("parameters", {})
1329
+ headers = comps.setdefault("headers", {})
1330
+
1331
+ params.setdefault(
1332
+ "IdempotencyKey",
1333
+ {
1334
+ "name": "Idempotency-Key",
1335
+ "in": "header",
1336
+ "required": False,
1337
+ "schema": {"type": "string"},
1338
+ "description": "Provide to make the request idempotent for 24h.",
1339
+ },
1340
+ )
1341
+ params.setdefault(
1342
+ "IfNoneMatch",
1343
+ {
1344
+ "name": "If-None-Match",
1345
+ "in": "header",
1346
+ "required": False,
1347
+ "schema": {"type": "string"},
1348
+ "description": "Conditional GET (ETag).",
1349
+ },
1350
+ )
1351
+ params.setdefault(
1352
+ "IfModifiedSince",
1353
+ {
1354
+ "name": "If-Modified-Since",
1355
+ "in": "header",
1356
+ "required": False,
1357
+ "schema": {"type": "string"},
1358
+ "description": "Conditional GET. HTTP-date per RFC 9110 (e.g. 'Wed, 01 Jan 2025 00:00:00 GMT').",
1359
+ },
1360
+ )
1361
+ params.setdefault(
1362
+ "IfMatch",
1363
+ {
1364
+ "name": "If-Match",
1365
+ "in": "header",
1366
+ "required": False,
1367
+ "schema": {"type": "string"},
1368
+ "description": "Optimistic concurrency for updates.",
1369
+ },
1370
+ )
1371
+
1372
+ headers.setdefault(
1373
+ "ETag", {"schema": {"type": "string"}, "description": "Opaque entity tag."}
1374
+ )
1375
+ headers.setdefault(
1376
+ "LastModified",
1377
+ {
1378
+ "schema": {"type": "string"}, # HTTP-date string
1379
+ "description": "Last modification time, HTTP-date per RFC 9110.",
1380
+ },
1381
+ )
1382
+ headers.setdefault(
1383
+ "XRateLimitLimit", {"schema": {"type": "integer"}, "description": "Tokens in window."}
1384
+ )
1385
+ headers.setdefault(
1386
+ "XRateLimitRemaining",
1387
+ {"schema": {"type": "integer"}, "description": "Remaining tokens."},
1388
+ )
1389
+ headers.setdefault(
1390
+ "XRateLimitReset", {"schema": {"type": "integer"}, "description": "Unix reset time."}
1391
+ )
1392
+ headers.setdefault(
1393
+ "XRequestId", {"schema": {"type": "string"}, "description": "Correlation id."}
1394
+ )
1395
+ headers.setdefault(
1396
+ "Deprecation",
1397
+ {
1398
+ "schema": {"type": "string"},
1399
+ "description": "Set to 'true' for deprecated endpoints.",
1400
+ },
1401
+ )
1402
+ headers.setdefault(
1403
+ "Sunset",
1404
+ {"schema": {"type": "string"}, "description": "HTTP-date for deprecation sunset."},
1405
+ )
1406
+ return schema
1407
+
1408
+ return m
1409
+
1410
+
1411
+ def attach_header_params_mutator():
1412
+ def m(schema: dict) -> dict:
1413
+ schema = dict(schema)
1414
+ comps = schema.setdefault("components", {})
1415
+ comp_params = comps.setdefault("parameters", {})
1416
+
1417
+ def _component_param_name(ref: str) -> tuple[str, str] | None:
1418
+ try:
1419
+ key = ref.rsplit("/", 1)[-1]
1420
+ p = comp_params.get(key) or {}
1421
+ name = p.get("name")
1422
+ where = p.get("in")
1423
+ if isinstance(name, str) and isinstance(where, str):
1424
+ return (name, where)
1425
+ except Exception:
1426
+ pass
1427
+ return None
1428
+
1429
+ for _, method, op in _iter_ops(schema):
1430
+ params = op.setdefault("parameters", [])
1431
+
1432
+ # What inline params (non-$ref) are already present?
1433
+ inline_names = {
1434
+ (p.get("name"), p.get("in"))
1435
+ for p in params
1436
+ if isinstance(p, dict) and "$ref" not in p and "name" in p and "in" in p
1437
+ }
1438
+
1439
+ def add_ref_if_absent(name: str):
1440
+ ref = {"$ref": f"#/components/parameters/{name}"}
1441
+ tup = _component_param_name(ref["$ref"])
1442
+ # If an inline with the same (name, in) exists (e.g., from a dependency),
1443
+ # don't add the ref at all.
1444
+ if tup and tup in inline_names:
1445
+ return
1446
+ params.append(ref)
1447
+
1448
+ # ---- Write methods: Idempotency-Key fallback (optional via $ref) ----
1449
+ # If the dependency added an inline required param, nothing to do;
1450
+ # otherwise, add the optional component ref so callers can still send it.
1451
+ if method in ("post", "patch", "delete"):
1452
+ if ("Idempotency-Key", "header") not in inline_names:
1453
+ add_ref_if_absent("IdempotencyKey")
1454
+ # Optional optimistic concurrency for updates
1455
+ if method in ("patch", "put"):
1456
+ add_ref_if_absent("IfMatch")
1457
+
1458
+ # ---- Conditional GET headers (optional via $ref) ----
1459
+ if method == "get":
1460
+ add_ref_if_absent("IfNoneMatch")
1461
+ add_ref_if_absent("IfModifiedSince")
1462
+
1463
+ # ---- Standard success/429 headers (unchanged) ----
1464
+ resps = op.get("responses") or {}
1465
+ for code, resp in resps.items():
1466
+ try:
1467
+ ic = int(code)
1468
+ except Exception:
1469
+ continue
1470
+ if 200 <= ic < 300:
1471
+ hdrs = resp.setdefault("headers", {})
1472
+ hdrs.setdefault("ETag", {"$ref": "#/components/headers/ETag"})
1473
+ hdrs.setdefault("Last-Modified", {"$ref": "#/components/headers/LastModified"})
1474
+ hdrs.setdefault("X-Request-Id", {"$ref": "#/components/headers/XRequestId"})
1475
+ hdrs.setdefault(
1476
+ "X-RateLimit-Limit", {"$ref": "#/components/headers/XRateLimitLimit"}
1477
+ )
1478
+ hdrs.setdefault(
1479
+ "X-RateLimit-Remaining",
1480
+ {"$ref": "#/components/headers/XRateLimitRemaining"},
1481
+ )
1482
+ hdrs.setdefault(
1483
+ "X-RateLimit-Reset", {"$ref": "#/components/headers/XRateLimitReset"}
1484
+ )
1485
+ if code == "429":
1486
+ resp.setdefault("headers", {})["Retry-After"] = {
1487
+ "schema": {"type": "integer"},
1488
+ "description": "Seconds until next allowed request.",
1489
+ }
1490
+
1491
+ return schema
1492
+
1493
+ return m
1494
+
1495
+
1496
+ def attach_conditional_get_304_mutator():
1497
+ def m(schema: dict) -> dict:
1498
+ schema = dict(schema)
1499
+ for _, method, op in _iter_ops(schema):
1500
+ if method != "get":
1501
+ continue
1502
+ resps = op.setdefault("responses", {})
1503
+ resps.setdefault(
1504
+ "304",
1505
+ {
1506
+ "description": "Not Modified",
1507
+ "headers": {
1508
+ "ETag": {"$ref": "#/components/headers/ETag"},
1509
+ "Last-Modified": {"$ref": "#/components/headers/LastModified"},
1510
+ "X-Request-Id": {"$ref": "#/components/headers/XRequestId"},
1511
+ },
1512
+ },
1513
+ )
1514
+ return schema
1515
+
1516
+ return m
1517
+
1518
+
814
1519
  def setup_mutators(
815
1520
  service: ServiceInfo,
816
1521
  spec: APIVersionSpec | None,
@@ -819,22 +1524,31 @@ def setup_mutators(
819
1524
  ) -> list:
820
1525
  mutators = [
821
1526
  conventions_mutator(),
1527
+ hardening_components_mutator(),
1528
+ attach_header_params_mutator(),
822
1529
  normalize_problem_and_examples_mutator(),
823
1530
  attach_standard_responses_mutator(),
1531
+ attach_conditional_get_304_mutator(),
824
1532
  auth_mutator(include_api_key),
825
1533
  strip_ref_siblings_in_responses_mutator(),
826
1534
  prune_invalid_responses_keys_mutator(),
1535
+ strip_ref_siblings_in_parameters_mutator(),
1536
+ dedupe_parameters_mutator(),
827
1537
  ensure_operation_descriptions_mutator(),
828
1538
  ensure_request_body_descriptions_mutator(),
829
1539
  ensure_parameter_metadata_mutator(),
830
1540
  ensure_media_type_schemas_mutator(),
831
1541
  ensure_examples_for_json_mutator(),
832
1542
  ensure_success_examples_mutator(),
1543
+ attach_default_tags_mutator(),
1544
+ attach_code_samples_mutator(),
1545
+ ensure_problem_examples_mutator(),
833
1546
  ensure_media_examples_mutator(),
834
1547
  scrub_invalid_object_examples_mutator(),
835
1548
  normalize_no_content_204_mutator(),
836
1549
  ensure_response_descriptions_mutator(),
837
1550
  improve_success_response_descriptions_mutator(),
1551
+ inject_safe_examples_mutator(),
838
1552
  dedupe_tags_mutator(),
839
1553
  ensure_global_tags_mutator(),
840
1554
  drop_unused_components_mutator_auto(),
@@ -842,5 +1556,4 @@ def setup_mutators(
842
1556
  ]
843
1557
  if server_url:
844
1558
  mutators.append(servers_mutator(server_url))
845
-
846
1559
  return mutators