azure-functions-openapi 0.4.0__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.
@@ -0,0 +1,3 @@
1
+ # src/azure_functions_openapi/__init__.py
2
+
3
+ __version__ = "0.4.0"
@@ -0,0 +1,143 @@
1
+ # src/azure_functions_openapi/decorator.py
2
+ from typing import Any, Callable, Dict, List, Optional, Type, TypeVar
3
+
4
+ from pydantic import BaseModel
5
+
6
+ # Define a generic type variable for functions
7
+ F = TypeVar("F", bound=Callable[..., Any])
8
+
9
+ # Global registry to hold OpenAPI metadata for each function
10
+ _openapi_registry: Dict[str, Dict[str, Any]] = {}
11
+
12
+
13
+ def openapi(
14
+ # ── basic metadata ───────────────────────────────────────────
15
+ summary: str = "",
16
+ description: str = "",
17
+ tags: Optional[List[str]] = None,
18
+ operation_id: Optional[str] = None,
19
+ # ── routing information ─────────────────────────────────────
20
+ route: Optional[str] = None,
21
+ method: Optional[str] = None,
22
+ parameters: Optional[List[Dict[str, Any]]] = None,
23
+ # ── request / response schema ───────────────────────────────
24
+ request_model: Optional[Type[BaseModel]] = None,
25
+ request_body: Optional[Dict[str, Any]] = None,
26
+ response_model: Optional[Type[BaseModel]] = None,
27
+ response: Optional[Dict[int, Dict[str, Any]]] = None,
28
+ ) -> Callable[[F], F]:
29
+ """
30
+ Decorator that attaches OpenAPI metadata to an Azure Functions handler.
31
+
32
+ Examples
33
+ --------
34
+ ### 1 · Minimal “Hello World”
35
+
36
+ ```python
37
+ @app.route(route="hello")
38
+ @openapi(summary="Hello", description="Returns plain text.")
39
+ def hello(req: func.HttpRequest) -> func.HttpResponse:
40
+ return func.HttpResponse("Hello, world!", status_code=200)
41
+ ```
42
+
43
+ ### 2 · Pydantic-powered JSON API
44
+
45
+ ```python
46
+ from pydantic import BaseModel
47
+
48
+ class TodoRequest(BaseModel):
49
+ title: str
50
+ done: bool = False
51
+
52
+ class TodoResponse(BaseModel):
53
+ id: int
54
+ title: str
55
+ done: bool
56
+
57
+ @app.route(route="todos/{id}", method="put")
58
+ @openapi(
59
+ summary="Update a todo item",
60
+ description=\"""Update a todo and return the updated document.\""",
61
+ tags=["Todo"],
62
+ parameters=[{"name": "id", "in": "path", "required": True, "schema": {"type": "integer"}}],
63
+ request_model=TodoRequest,
64
+ response_model=TodoResponse,
65
+ operation_id="updateTodo",
66
+ )
67
+ def update_todo(req: func.HttpRequest) -> func.HttpResponse:
68
+ # ... business logic ...
69
+ body = TodoRequest.model_validate_json(req.get_body())
70
+ todo = TodoResponse(id=1, **body.model_dump())
71
+ return func.HttpResponse(
72
+ todo.model_dump_json(),
73
+ status_code=200,
74
+ mimetype="application/json",
75
+ )
76
+ ```
77
+
78
+ After starting the Function App you get:
79
+
80
+ * **Swagger UI** → `http://localhost:7071/api/docs`
81
+ * **Raw JSON spec** → `http://localhost:7071/api/openapi.json`
82
+
83
+ Parameters
84
+ ----------
85
+ summary:
86
+ Short description shown in Swagger UI.
87
+ description:
88
+ Longer Markdown-enabled description.
89
+ tags:
90
+ List of group tags.
91
+ operation_id:
92
+ Custom operationId (defaults to function name).
93
+ route:
94
+ Override for the HTTP route path (e.g. "/items/{id}").
95
+ method:
96
+ Explicit HTTP method if not inferrable.
97
+ parameters:
98
+ List of param objects (query/path/header/cookie).
99
+ request_model:
100
+ Pydantic model used to derive requestBody schema.
101
+ request_body:
102
+ Raw requestBody schema (if you don’t use Pydantic).
103
+ response_model:
104
+ Pydantic model used to derive 200-response schema.
105
+ response:
106
+ Manual responses dict keyed by status code.
107
+
108
+ Returns
109
+ -------
110
+ Callable
111
+ The original function, with its name stored in `_openapi_registry`.
112
+ """
113
+
114
+ def decorator(func: F) -> F:
115
+ _openapi_registry[func.__name__] = {
116
+ # ── basic metadata ────────────────────────────────
117
+ "summary": summary,
118
+ "description": description,
119
+ "tags": tags or ["default"],
120
+ "operation_id": operation_id,
121
+ # ── routing info ─────────────────────────────────
122
+ "route": route,
123
+ "method": method,
124
+ "parameters": parameters or [],
125
+ # ── request / response schema ────────────────────
126
+ "request_model": request_model,
127
+ "request_body": request_body,
128
+ "response_model": response_model,
129
+ "response": response or {},
130
+ }
131
+ return func
132
+
133
+ return decorator
134
+
135
+
136
+ def get_openapi_registry() -> Dict[str, Dict[str, Any]]:
137
+ """
138
+ Retrieve OpenAPI metadata for all registered functions.
139
+
140
+ Returns:
141
+ A dictionary where each key is a function name and value is its OpenAPI metadata.
142
+ """
143
+ return _openapi_registry
@@ -0,0 +1,99 @@
1
+ # src/azure_functions_openapi/openapi.py
2
+ import json
3
+ from typing import Any, Dict, List
4
+
5
+ import yaml
6
+
7
+ from azure_functions_openapi.decorator import get_openapi_registry
8
+ from azure_functions_openapi.utils import model_to_schema
9
+
10
+
11
+ def generate_openapi_spec(title: str = "API", version: str = "1.0.0") -> Dict[str, Any]:
12
+ """
13
+ Compile an OpenAPI-3 specification from the registry.
14
+ No base-path is added; `route=` is used exactly as provided.
15
+ """
16
+ registry = get_openapi_registry()
17
+ paths: Dict[str, Dict[str, Any]] = {}
18
+
19
+ for func_name, meta in registry.items():
20
+ # route & method --------------------------------------------------
21
+ path = meta.get("route") or f"/{func_name}"
22
+ method = (meta.get("method") or "get").lower()
23
+
24
+ # responses -------------------------------------------------------
25
+ responses: Dict[str, Any] = {}
26
+ for status, detail in meta.get("response", {}).items():
27
+ resp = {"description": detail.get("description", "")}
28
+ if "content" in detail:
29
+ resp["content"] = detail["content"]
30
+ responses[str(status)] = resp
31
+
32
+ if meta.get("response_model"):
33
+ responses["200"] = {
34
+ "description": "Successful Response",
35
+ "content": {
36
+ "application/json": {"schema": model_to_schema(meta["response_model"])}
37
+ },
38
+ }
39
+
40
+ # operation object ------------------------------------------------
41
+ op: Dict[str, Any] = {
42
+ "summary": meta.get("summary", ""),
43
+ "description": meta.get("description", ""),
44
+ "operationId": meta.get("operation_id") or f"{method}_{func_name}",
45
+ "tags": meta.get("tags") or ["default"],
46
+ "responses": responses,
47
+ }
48
+
49
+ # parameters ------------------------------------------------------
50
+ parameters: List[Dict[str, Any]] = meta.get("parameters", [])
51
+ if parameters:
52
+ op["parameters"] = parameters
53
+
54
+ # requestBody (POST/PUT/PATCH) ------------------------------------
55
+ if method in {"post", "put", "patch"}:
56
+ if meta.get("request_body"):
57
+ op["requestBody"] = {
58
+ "required": True,
59
+ "content": {"application/json": {"schema": meta["request_body"]}},
60
+ }
61
+ elif meta.get("request_model"):
62
+ op["requestBody"] = {
63
+ "required": True,
64
+ "content": {
65
+ "application/json": {"schema": model_to_schema(meta["request_model"])}
66
+ },
67
+ }
68
+
69
+ # merge into paths (support multiple methods per route) ----------
70
+ paths.setdefault(path, {})[method] = op
71
+
72
+ return {
73
+ "openapi": "3.0.0",
74
+ "info": {
75
+ "title": title,
76
+ "version": version,
77
+ "description": (
78
+ "Auto-generated OpenAPI documentation. "
79
+ "Markdown supported in descriptions (CommonMark)."
80
+ ),
81
+ },
82
+ "paths": paths,
83
+ }
84
+
85
+
86
+ def get_openapi_json() -> str:
87
+ """Return the spec as pretty-printed JSON (UTF-8).
88
+ Returns:
89
+ str: OpenAPI spec in JSON format.
90
+ """
91
+ return json.dumps(generate_openapi_spec(), indent=2, ensure_ascii=False)
92
+
93
+
94
+ def get_openapi_yaml() -> str:
95
+ """Return the spec as YAML.
96
+ Returns:
97
+ str: OpenAPI spec in YAML format.
98
+ """
99
+ return yaml.safe_dump(generate_openapi_spec(), sort_keys=False, allow_unicode=True)
@@ -0,0 +1,28 @@
1
+ from azure.functions import HttpResponse
2
+
3
+
4
+ def render_swagger_ui() -> HttpResponse:
5
+ html_content = """
6
+ <!DOCTYPE html>
7
+ <html>
8
+ <head>
9
+ <title>Swagger UI</title>
10
+ <link rel="stylesheet"
11
+ type="text/css"
12
+ href="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui.css" />
13
+ </head>
14
+ <body>
15
+ <div id="swagger-ui"></div>
16
+ <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-bundle.js"></script>
17
+ <script>
18
+ const ui = SwaggerUIBundle({
19
+ url: '/api/openapi.json',
20
+ dom_id: '#swagger-ui',
21
+ presets: [SwaggerUIBundle.presets.apis],
22
+ layout: 'BaseLayout'
23
+ });
24
+ </script>
25
+ </body>
26
+ </html>
27
+ """
28
+ return HttpResponse(html_content, mimetype="text/html")
@@ -0,0 +1,20 @@
1
+ # src/azure_functions_openapi/utils.py
2
+ from typing import Any, Dict, cast
3
+
4
+ from packaging import version
5
+ import pydantic
6
+
7
+ PYDANTIC_V2 = version.parse(pydantic.__version__) >= version.parse("2.0.0")
8
+
9
+
10
+ def model_to_schema(model_cls: Any) -> Dict[str, Any]:
11
+ """Return JSON schema from a Pydantic model class.
12
+ Parameters:
13
+ model_cls: Pydantic model class.
14
+ Returns:
15
+ Dict[str, Any]: JSON schema representation of the model.
16
+ """
17
+
18
+ if PYDANTIC_V2:
19
+ return cast(Dict[str, Any], model_cls.model_json_schema())
20
+ return cast(Dict[str, Any], model_cls.schema())
@@ -0,0 +1,244 @@
1
+ Metadata-Version: 2.4
2
+ Name: azure-functions-openapi
3
+ Version: 0.4.0
4
+ Summary: OpenAPI (Swagger) integration for Python-based Azure Functions
5
+ Author-email: Yeongseon Choe <yeongseon.choe@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: <3.13,>=3.9
9
+ Requires-Dist: azure-functions
10
+ Requires-Dist: pydantic<3.0,>=1.10
11
+ Requires-Dist: pyyaml
12
+ Provides-Extra: dev
13
+ Requires-Dist: bandit; extra == 'dev'
14
+ Requires-Dist: black; extra == 'dev'
15
+ Requires-Dist: git-changelog; extra == 'dev'
16
+ Requires-Dist: hatch; extra == 'dev'
17
+ Requires-Dist: mypy; extra == 'dev'
18
+ Requires-Dist: pre-commit; extra == 'dev'
19
+ Requires-Dist: pytest; extra == 'dev'
20
+ Requires-Dist: pytest-cov; extra == 'dev'
21
+ Requires-Dist: ruff; extra == 'dev'
22
+ Requires-Dist: types-pyyaml; extra == 'dev'
23
+ Provides-Extra: docs
24
+ Requires-Dist: mkdocs; extra == 'docs'
25
+ Requires-Dist: mkdocs-material; extra == 'docs'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # azure-functions-openapi
29
+
30
+ [![PyPI](https://img.shields.io/pypi/v/azure-functions-openapi.svg)](https://pypi.org/project/azure-functions-openapi/)
31
+ [![Python Version](https://img.shields.io/pypi/pyversions/azure-functions-openapi.svg)](https://pypi.org/project/azure-functions-openapi/)
32
+ [![CI](https://github.com/yeongseon/azure-functions-openapi/actions/workflows/test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi/actions/workflows/test.yml)
33
+ [![codecov](https://codecov.io/gh/yeongseon/azure-functions-openapi/branch/main/graph/badge.svg)](https://codecov.io/gh/yeongseon/azure-functions-openapi)
34
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://pre-commit.com/)
35
+ [![Docs](https://img.shields.io/badge/docs-gh--pages-blue)](https://yeongseon.github.io/azure-functions-openapi/)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
37
+
38
+ > Effortless OpenAPI (Swagger) documentation & Swagger‑UI for **Python Azure Functions**.
39
+
40
+ ---
41
+
42
+ ## Features
43
+
44
+ - `@openapi` decorator — annotate once, generate full spec
45
+ - Serves `/openapi.json`, `/openapi.yaml`, and `/docs` (Swagger UI)
46
+ - Supports query/path/header parameters, requestBody, responses, tags
47
+ - Optional Pydantic integration (supports both v1 and v2)
48
+ - Zero hard dependency on Pydantic
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install azure-functions-openapi
56
+ ```
57
+
58
+ For local development:
59
+
60
+ ```bash
61
+ git clone https://github.com/yeongseon/azure-functions-openapi.git
62
+ cd azure-functions-openapi
63
+ pip install -e .[dev]
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Quick Start
69
+
70
+ > Create a minimal HTTP-triggered Azure Function with auto Swagger documentation.
71
+
72
+ 1. Set up environment
73
+ ```bash
74
+ python -m venv .venv
75
+ source .venv/bin/activate
76
+ pip install azure-functions azure-functions-worker azure-functions-openapi
77
+ ```
78
+
79
+ 2. Initialize Azure Functions project
80
+ ```bash
81
+ func init hello_openapi --python
82
+ cd hello_openapi
83
+ ```
84
+
85
+ 3. Add `function_app.py` with OpenAPI-decorated function and endpoints:
86
+ ```python
87
+ # hello_openapi/function_app.py
88
+
89
+ import json
90
+ import azure.functions as func
91
+ from azure_functions_openapi.decorator import openapi
92
+ from azure_functions_openapi.openapi import get_openapi_json, get_openapi_yaml
93
+ from azure_functions_openapi.swagger_ui import render_swagger_ui
94
+
95
+ app = func.FunctionApp()
96
+
97
+ @openapi(
98
+ summary="Greet user",
99
+ route="/api/http_trigger",
100
+ request_model={"name": "string"},
101
+ response_model={"message": "string"},
102
+ tags=["Example"]
103
+ )
104
+ @app.function_name(name="http_trigger")
105
+ @app.route(route="/api/http_trigger", auth_level=func.AuthLevel.ANONYMOUS, methods=["POST"])
106
+ def main(req: func.HttpRequest) -> func.HttpResponse:
107
+ try:
108
+ data = req.get_json()
109
+ name = data.get("name", "world")
110
+ return func.HttpResponse(
111
+ json.dumps({"message": f"Hello, {name}!"}),
112
+ mimetype="application/json"
113
+ )
114
+ except Exception as e:
115
+ return func.HttpResponse(f"Error: {str(e)}", status_code=400)
116
+
117
+ @app.function_name(name="openapi_json")
118
+ @app.route(route="/api/openapi.json", auth_level=func.AuthLevel.ANONYMOUS, methods=["GET"])
119
+ def openapi_json(req: func.HttpRequest) -> func.HttpResponse:
120
+ return get_openapi_json()
121
+
122
+ @app.function_name(name="openapi_yaml")
123
+ @app.route(route="/api/openapi.yaml", auth_level=func.AuthLevel.ANONYMOUS, methods=["GET"])
124
+ def openapi_yaml(req: func.HttpRequest) -> func.HttpResponse:
125
+ return get_openapi_yaml()
126
+
127
+ @app.function_name(name="swagger_ui")
128
+ @app.route(route="/api/docs", auth_level=func.AuthLevel.ANONYMOUS, methods=["GET"])
129
+ def swagger_ui(req: func.HttpRequest) -> func.HttpResponse:
130
+ return render_swagger_ui()
131
+ ```
132
+ > Swagger UI (`/docs`) is now supported via `render_swagger_ui()` helper.
133
+
134
+ 4. Run locally:
135
+ ```bash
136
+ func start
137
+ ```
138
+
139
+ - OpenAPI JSON: http://localhost:7071/api/openapi.json
140
+ - Swagger UI: http://localhost:7071/api/docs
141
+
142
+ 5. Deploy:
143
+ ```bash
144
+ func azure functionapp publish <FUNCTION-APP-NAME> --python
145
+ ```
146
+
147
+ - OpenAPI JSON: https://<FUNCTION-APP-NAME>.azurewebsites.net/api/openapi.json
148
+
149
+ A partial example of the generated `/api/openapi.json`:
150
+
151
+ ```json
152
+ {
153
+ "openapi": "3.0.0",
154
+ "info": {
155
+ "title": "API",
156
+ "version": "1.0.0",
157
+ "description": "Auto-generated OpenAPI documentation. Markdown supported in descriptions (CommonMark)."
158
+ },
159
+ "paths": {
160
+ "/api/http_trigger": {
161
+ "get": {
162
+ "summary": "HTTP Trigger with name parameter",
163
+ "description": "Returns a greeting using the **name** from query or body.",
164
+ "parameters": [
165
+ {
166
+ "name": "name",
167
+ "in": "query",
168
+ "required": true,
169
+ "schema": { "type": "string" },
170
+ "description": "Name to greet"
171
+ }
172
+ ],
173
+ "responses": {
174
+ "200": {
175
+ "description": "Successful response with greeting",
176
+ "content": {
177
+ "application/json": {
178
+ "examples": {
179
+ "sample": {
180
+ "summary": "Example greeting",
181
+ "value": {
182
+ "message": "Hello, Azure!"
183
+ }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ },
189
+ "400": { "description": "Invalid request" }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ }
195
+ ```
196
+
197
+ - Swagger UI: https://<FUNCTION-APP-NAME>.azurewebsites.net/api/docs
198
+
199
+ Swagger UI will look once you set up the routes:
200
+
201
+ ![Swagger UI Example](./docs/assets/hello_openapi_swagger_ui_preview.png)
202
+
203
+ ---
204
+
205
+ ## Example with Pydantic
206
+
207
+ ```python
208
+ from pydantic import BaseModel
209
+ from azure_functions_openapi.decorator import openapi
210
+
211
+ class RequestModel(BaseModel):
212
+ name: str
213
+
214
+ class ResponseModel(BaseModel):
215
+ message: str
216
+
217
+ @openapi(
218
+ summary="Greet user (Pydantic)",
219
+ route="/api/http_trigger",
220
+ request_model=RequestModel,
221
+ response_model=ResponseModel,
222
+ tags=["Example"]
223
+ )
224
+ def http_trigger(req: func.HttpRequest) -> func.HttpResponse:
225
+ ...
226
+ ```
227
+
228
+ > Supports both Pydantic v1 and v2.
229
+ Schema inference will work automatically with either version.
230
+
231
+ ---
232
+
233
+ ## Documentation
234
+
235
+ - Full docs: [yeongseon.github.io/azure-functions-openapi](https://yeongseon.github.io/azure-functions-openapi/)
236
+ - [Quickstart](docs/usage.md)
237
+ - [Development Guide](docs/development.md)
238
+ - [Contribution Guide](docs/contributing.md)
239
+
240
+ ---
241
+
242
+ ## License
243
+
244
+ MIT © 2025 Yeongseon Choe
@@ -0,0 +1,9 @@
1
+ azure_functions_openapi/__init__.py,sha256=3espqW3_xVQoBqi0ha08IP22z55rFwElswG-a0ao18c,65
2
+ azure_functions_openapi/decorator.py,sha256=7rDj-MD-PKSvZHSjCFF-OIRo2sLygyNeF2hA2T58O0g,5009
3
+ azure_functions_openapi/openapi.py,sha256=0X35QpYDRxT2FblaMiQyhKHRCrpmK4bgQNcIvXBOCxA,3558
4
+ azure_functions_openapi/swagger_ui.py,sha256=AGB4oAlQA3ayMREk5UskNwfahifOcPh_jzfTyK5z3ok,834
5
+ azure_functions_openapi/utils.py,sha256=xzZn7-DfC_OktsuS3WTFiDruTBeNGOwxnKhYvIBsqps,593
6
+ azure_functions_openapi-0.4.0.dist-info/METADATA,sha256=G5ZCaw72tMl1VTfj_mDhgNuludQX4nQXI6sECP0iGSg,7245
7
+ azure_functions_openapi-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ azure_functions_openapi-0.4.0.dist-info/licenses/LICENSE,sha256=gBTzk1NFKuyZKqa5-8leVY816k7POZUMpw2_PKl2MsY,1071
9
+ azure_functions_openapi-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yeongseon Choe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.