Skip to content

Commit fd4522d

Browse files
feat(openapi): add support for micro Lambda pattern (#7920)
* feat: adding openapi micro lambda support * feat: adding openapi micro lambda support * feat: adding openapi micro lambda support * feat: adding openapi micro lambda support * tests: improve coverage * addressing copilot suggestions * making xenon happy...
1 parent 4602ffc commit fd4522d

File tree

17 files changed

+1393
-25
lines changed

17 files changed

+1393
-25
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 187 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2206,6 +2206,162 @@ def configure_openapi(
22062206
openapi_extensions=openapi_extensions,
22072207
)
22082208

2209+
def configure_openapi_merge(
2210+
self,
2211+
path: str,
2212+
pattern: str | list[str] = "handler.py",
2213+
exclude: list[str] | None = None,
2214+
resolver_name: str = "app",
2215+
recursive: bool = False,
2216+
title: str = DEFAULT_OPENAPI_TITLE,
2217+
version: str = DEFAULT_API_VERSION,
2218+
openapi_version: str = DEFAULT_OPENAPI_VERSION,
2219+
summary: str | None = None,
2220+
description: str | None = None,
2221+
tags: list[Tag | str] | None = None,
2222+
servers: list[Server] | None = None,
2223+
terms_of_service: str | None = None,
2224+
contact: Contact | None = None,
2225+
license_info: License | None = None,
2226+
security_schemes: dict[str, SecurityScheme] | None = None,
2227+
security: list[dict[str, list[str]]] | None = None,
2228+
external_documentation: ExternalDocumentation | None = None,
2229+
openapi_extensions: dict[str, Any] | None = None,
2230+
on_conflict: Literal["warn", "error", "first", "last"] = "warn",
2231+
):
2232+
"""Configure OpenAPI merge to generate a unified schema from multiple Lambda handlers.
2233+
2234+
This method discovers resolver instances across multiple Python files and merges
2235+
their OpenAPI schemas into a single unified specification. Useful for micro-function
2236+
architectures where each Lambda has its own resolver.
2237+
2238+
Parameters
2239+
----------
2240+
path : str
2241+
Root directory path to search for resolver files.
2242+
pattern : str | list[str], optional
2243+
Glob pattern(s) to match handler files. Default is "handler.py".
2244+
exclude : list[str], optional
2245+
Patterns to exclude from search. Default excludes tests, __pycache__, and .venv.
2246+
resolver_name : str, optional
2247+
Name of the resolver variable in handler files. Default is "app".
2248+
recursive : bool, optional
2249+
Whether to search recursively in subdirectories. Default is False.
2250+
title : str
2251+
The title of the unified API.
2252+
version : str
2253+
The version of the OpenAPI document.
2254+
openapi_version : str, default = "3.1.0"
2255+
The version of the OpenAPI Specification.
2256+
summary : str, optional
2257+
A short summary of what the application does.
2258+
description : str, optional
2259+
A verbose explanation of the application behavior.
2260+
tags : list[Tag | str], optional
2261+
A list of tags used by the specification with additional metadata.
2262+
servers : list[Server], optional
2263+
An array of Server Objects for connectivity information.
2264+
terms_of_service : str, optional
2265+
A URL to the Terms of Service for the API.
2266+
contact : Contact, optional
2267+
The contact information for the exposed API.
2268+
license_info : License, optional
2269+
The license information for the exposed API.
2270+
security_schemes : dict[str, SecurityScheme], optional
2271+
Security schemes available in the specification.
2272+
security : list[dict[str, list[str]]], optional
2273+
Security mechanisms applied globally across the API.
2274+
external_documentation : ExternalDocumentation, optional
2275+
A link to external documentation for the API.
2276+
openapi_extensions : dict[str, Any], optional
2277+
Additional OpenAPI extensions as a dictionary.
2278+
on_conflict : str, optional
2279+
Strategy for handling conflicts when the same path+method is defined
2280+
in multiple schemas. Options: "warn" (default), "error", "first", "last".
2281+
2282+
Example
2283+
-------
2284+
>>> from aws_lambda_powertools.event_handler import APIGatewayRestResolver
2285+
>>>
2286+
>>> app = APIGatewayRestResolver()
2287+
>>> app.configure_openapi_merge(
2288+
... path="./functions",
2289+
... pattern="handler.py",
2290+
... exclude=["**/tests/**"],
2291+
... resolver_name="app",
2292+
... title="My Unified API",
2293+
... version="1.0.0",
2294+
... )
2295+
2296+
See Also
2297+
--------
2298+
configure_openapi : Configure OpenAPI for a single resolver
2299+
enable_swagger : Enable Swagger UI
2300+
"""
2301+
from aws_lambda_powertools.event_handler.openapi.merge import OpenAPIMerge
2302+
2303+
if exclude is None:
2304+
exclude = ["**/tests/**", "**/__pycache__/**", "**/.venv/**"]
2305+
2306+
self._openapi_merge = OpenAPIMerge(
2307+
title=title,
2308+
version=version,
2309+
openapi_version=openapi_version,
2310+
summary=summary,
2311+
description=description,
2312+
tags=tags,
2313+
servers=servers,
2314+
terms_of_service=terms_of_service,
2315+
contact=contact,
2316+
license_info=license_info,
2317+
security_schemes=security_schemes,
2318+
security=security,
2319+
external_documentation=external_documentation,
2320+
openapi_extensions=openapi_extensions,
2321+
on_conflict=on_conflict,
2322+
)
2323+
self._openapi_merge.discover(
2324+
path=path,
2325+
pattern=pattern,
2326+
exclude=exclude,
2327+
resolver_name=resolver_name,
2328+
recursive=recursive,
2329+
)
2330+
2331+
def get_openapi_merge_schema(self) -> dict[str, Any]:
2332+
"""Get the merged OpenAPI schema from multiple Lambda handlers.
2333+
2334+
Returns
2335+
-------
2336+
dict[str, Any]
2337+
The merged OpenAPI schema.
2338+
2339+
Raises
2340+
------
2341+
RuntimeError
2342+
If configure_openapi_merge has not been called.
2343+
"""
2344+
if not hasattr(self, "_openapi_merge") or self._openapi_merge is None:
2345+
raise RuntimeError("configure_openapi_merge must be called before get_openapi_merge_schema")
2346+
return self._openapi_merge.get_openapi_schema()
2347+
2348+
def get_openapi_merge_json_schema(self) -> str:
2349+
"""Get the merged OpenAPI schema as JSON from multiple Lambda handlers.
2350+
2351+
Returns
2352+
-------
2353+
str
2354+
The merged OpenAPI schema as a JSON string.
2355+
2356+
Raises
2357+
------
2358+
RuntimeError
2359+
If configure_openapi_merge has not been called.
2360+
"""
2361+
if not hasattr(self, "_openapi_merge") or self._openapi_merge is None:
2362+
raise RuntimeError("configure_openapi_merge must be called before get_openapi_merge_json_schema")
2363+
return self._openapi_merge.get_openapi_json_schema()
2364+
22092365
def enable_swagger(
22102366
self,
22112367
*,
@@ -2312,32 +2468,38 @@ def swagger_handler():
23122468

23132469
openapi_servers = servers or [Server(url=(base_path or "/"))]
23142470

2315-
spec = self.get_openapi_schema(
2316-
title=title,
2317-
version=version,
2318-
openapi_version=openapi_version,
2319-
summary=summary,
2320-
description=description,
2321-
tags=tags,
2322-
servers=openapi_servers,
2323-
terms_of_service=terms_of_service,
2324-
contact=contact,
2325-
license_info=license_info,
2326-
security_schemes=security_schemes,
2327-
security=security,
2328-
external_documentation=external_documentation,
2329-
openapi_extensions=openapi_extensions,
2330-
)
2471+
# Use merged schema if configure_openapi_merge was called, otherwise use regular schema
2472+
if hasattr(self, "_openapi_merge") and self._openapi_merge is not None:
2473+
# Get merged schema as JSON string (already properly serialized)
2474+
escaped_spec = self._openapi_merge.get_openapi_json_schema().replace("</", "<\\/")
2475+
else:
2476+
spec = self.get_openapi_schema(
2477+
title=title,
2478+
version=version,
2479+
openapi_version=openapi_version,
2480+
summary=summary,
2481+
description=description,
2482+
tags=tags,
2483+
servers=openapi_servers,
2484+
terms_of_service=terms_of_service,
2485+
contact=contact,
2486+
license_info=license_info,
2487+
security_schemes=security_schemes,
2488+
security=security,
2489+
external_documentation=external_documentation,
2490+
openapi_extensions=openapi_extensions,
2491+
)
23312492

2332-
# The .replace('</', '<\\/') part is necessary to prevent a potential issue where the JSON string contains
2333-
# </script> or similar tags. Escaping the forward slash in </ as <\/ ensures that the JSON does not
2334-
# inadvertently close the script tag, and the JSON remains a valid string within the JavaScript code.
2335-
escaped_spec = model_json(
2336-
spec,
2337-
by_alias=True,
2338-
exclude_none=True,
2339-
indent=2,
2340-
).replace("</", "<\\/")
2493+
# The .replace('</', '<\\/') part is necessary to prevent a potential issue where the JSON
2494+
# string contains </script> or similar tags. Escaping the forward slash in </ as <\/ ensures
2495+
# that the JSON does not inadvertently close the script tag, and the JSON remains a valid
2496+
# string within the JavaScript code.
2497+
escaped_spec = model_json(
2498+
spec,
2499+
by_alias=True,
2500+
exclude_none=True,
2501+
indent=2,
2502+
).replace("</", "<\\/")
23412503

23422504
# Check for query parameters; if "format" is specified as "json",
23432505
# respond with the JSON used in the OpenAPI spec
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""OpenAPI module for AWS Lambda Powertools."""
2+
3+
from aws_lambda_powertools.event_handler.openapi.exceptions import OpenAPIMergeError
4+
from aws_lambda_powertools.event_handler.openapi.merge import OpenAPIMerge
5+
6+
__all__ = [
7+
"OpenAPIMerge",
8+
"OpenAPIMergeError",
9+
]

aws_lambda_powertools/event_handler/openapi/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,7 @@ class SchemaValidationError(ValidationException):
4545
"""
4646
Raised when the OpenAPI schema validation fails
4747
"""
48+
49+
50+
class OpenAPIMergeError(Exception):
51+
"""Exception raised when there's a conflict during OpenAPI merge."""

0 commit comments

Comments
 (0)