fastopenapi 0.1.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nikita Ryzhenkov
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.
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.3
2
+ Name: fastopenapi
3
+ Version: 0.1.0
4
+ Summary: A library for generating and integrating OpenAPI schemas using Pydantic v2.
5
+ License: MIT
6
+ Keywords: FastOpenAPI,OpenAPI,Pydantic,API,Python,Flask,Falcon,Sanic,Starlette
7
+ Author: Nikita Ryzhenkov
8
+ Author-email: nikita.ryzhenkoff@gmail.com
9
+ Requires-Python: >=3.10
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Classifier: Topic :: Software Development :: Code Generators
18
+ Provides-Extra: falcon
19
+ Provides-Extra: flask
20
+ Provides-Extra: sanic
21
+ Provides-Extra: starlette
22
+ Requires-Dist: falcon (>=4.0.2,<5.0.0) ; extra == "falcon"
23
+ Requires-Dist: flask (>=3.1.0,<4.0.0) ; extra == "flask"
24
+ Requires-Dist: pydantic (>=2.10.6,<3.0.0)
25
+ Requires-Dist: sanic (>=24.12.0,<25.0.0) ; extra == "sanic"
26
+ Requires-Dist: starlette (>=0.46.0,<0.47.0) ; extra == "starlette"
27
+ Description-Content-Type: text/markdown
28
+
29
+
30
+ # 🚀 FastOpenAPI
31
+ ![Test](https://github.com/mr-fatalyst/fastopenapi/actions/workflows/test.yml/badge.svg)
32
+ ![codecov](https://codecov.io/gh/mr-fatalyst/fastopenapi/branch/main/graph/badge.svg?token=USHR1I0CJB)
33
+
34
+ **FastOpenAPI** is a library for generating and integrating OpenAPI schemas using Pydantic v2 and various frameworks (Falcon, Flask, Sanic, Starlette).
35
+
36
+ ---
37
+
38
+ ## 📦 Installation
39
+ ```bash
40
+ pip install fastopenapi
41
+ ```
42
+
43
+ ---
44
+
45
+ ## ⚙️ Features
46
+ - 📄 **Generate OpenAPI schemas** with Pydantic v2.
47
+ - 🛡️ **Data validation** using Pydantic models.
48
+ - 🛠️ **Supports multiple frameworks:** Falcon, Flask, Sanic, Starlette.
49
+ - ✅ **Compatible with Pydantic v2.**
50
+
51
+ ---
52
+
53
+ ## 🛠️ Quick Start
54
+
55
+ ### ![Falcon](https://img.shields.io/badge/Falcon-45b8d8?style=flat&logo=falcon&logoColor=white)
56
+ ```python
57
+ import falcon.asgi
58
+ import uvicorn
59
+ from pydantic import BaseModel
60
+
61
+ from fastopenapi.routers.falcon import FalconRouter
62
+
63
+ app = falcon.asgi.App()
64
+ router = FalconRouter(app=app, docs_url="/docs/", openapi_version="3.0.0")
65
+
66
+
67
+ class HelloResponse(BaseModel):
68
+ message: str
69
+
70
+
71
+ @router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
72
+ async def hello(name: str):
73
+ """Say hello from Falcon"""
74
+ return HelloResponse(message=f"Hello, {name}! It's Falcon!")
75
+
76
+
77
+ if __name__ == "__main__":
78
+ uvicorn.run(app, host="127.0.0.1", port=8000)
79
+
80
+ ```
81
+
82
+ ---
83
+
84
+ ### ![Flask](https://img.shields.io/badge/-Flask-000000?style=flat-square&logo=flask&logoColor=white)
85
+ ```python
86
+ from flask import Flask
87
+ from pydantic import BaseModel
88
+
89
+ from fastopenapi.routers.flask import FlaskRouter
90
+
91
+ app = Flask(__name__)
92
+ router = FlaskRouter(app=app, docs_url="/docs/", openapi_version="3.0.0")
93
+
94
+
95
+ class HelloResponse(BaseModel):
96
+ message: str
97
+
98
+
99
+ @router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
100
+ def hello(name: str):
101
+ """Say hello from Flask"""
102
+ return HelloResponse(message=f"Hello, {name}! It's Flask!")
103
+
104
+
105
+ if __name__ == "__main__":
106
+ app.run(debug=True, port=8000)
107
+
108
+ ```
109
+
110
+ ---
111
+
112
+ ### ![Sanic](https://img.shields.io/badge/-Sanic-00bfff?style=flat-square&logo=sanic&logoColor=white)
113
+ ```python
114
+ from pydantic import BaseModel
115
+ from sanic import Sanic
116
+
117
+ from fastopenapi.routers.sanic import SanicRouter
118
+
119
+ app = Sanic("MySanicApp")
120
+ router = SanicRouter(app=app, docs_url="/docs/", openapi_version="3.0.0")
121
+
122
+
123
+ class HelloResponse(BaseModel):
124
+ message: str
125
+
126
+
127
+ @router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
128
+ async def hello(name: str):
129
+ """Say hello from Sanic"""
130
+ return HelloResponse(message=f"Hello, {name}! It's Sanic!")
131
+
132
+
133
+ if __name__ == "__main__":
134
+ app.run(host="0.0.0.0", port=8000, debug=True)
135
+
136
+ ```
137
+
138
+ ---
139
+
140
+ ### ![Starlette](https://img.shields.io/badge/-Starlette-ff4785?style=flat-square&logo=starlette&logoColor=white)
141
+ ```python
142
+ import uvicorn
143
+ from pydantic import BaseModel
144
+ from starlette.applications import Starlette
145
+
146
+ from fastopenapi.routers.starlette import StarletteRouter
147
+
148
+ app = Starlette()
149
+ router = StarletteRouter(app=app, docs_url="/docs/", openapi_version="3.0.0")
150
+
151
+
152
+ class HelloResponse(BaseModel):
153
+ message: str
154
+
155
+
156
+ @router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
157
+ async def hello(name: str):
158
+ """Say hello from Starlette"""
159
+ return HelloResponse(message=f"Hello, {name}! It's Starlette!")
160
+
161
+
162
+ router.register_routes()
163
+
164
+ if __name__ == "__main__":
165
+ uvicorn.run(app, host="127.0.0.1", port=8000)
166
+
167
+ ```
168
+
169
+ ---
170
+
171
+ ## 🛡️ **Type Safety with Pydantic v2**
172
+ ```python
173
+ from pydantic import BaseModel
174
+
175
+ class User(BaseModel):
176
+ id: int
177
+ name: str
178
+
179
+ @router.post("/api/v1/users/")
180
+ def create_user(user: User) -> User:
181
+ return user
182
+ ```
183
+
184
+ ---
185
+
186
+ ## 🧪 **Running Tests**
187
+ ```bash
188
+ poetry run pytest
189
+ ```
190
+
191
+ ---
192
+
193
+ ## 📄 **License**
194
+ This project is licensed under the terms of the MIT license.
195
+
@@ -0,0 +1,166 @@
1
+
2
+ # 🚀 FastOpenAPI
3
+ ![Test](https://github.com/mr-fatalyst/fastopenapi/actions/workflows/test.yml/badge.svg)
4
+ ![codecov](https://codecov.io/gh/mr-fatalyst/fastopenapi/branch/main/graph/badge.svg?token=USHR1I0CJB)
5
+
6
+ **FastOpenAPI** is a library for generating and integrating OpenAPI schemas using Pydantic v2 and various frameworks (Falcon, Flask, Sanic, Starlette).
7
+
8
+ ---
9
+
10
+ ## 📦 Installation
11
+ ```bash
12
+ pip install fastopenapi
13
+ ```
14
+
15
+ ---
16
+
17
+ ## ⚙️ Features
18
+ - 📄 **Generate OpenAPI schemas** with Pydantic v2.
19
+ - 🛡️ **Data validation** using Pydantic models.
20
+ - 🛠️ **Supports multiple frameworks:** Falcon, Flask, Sanic, Starlette.
21
+ - ✅ **Compatible with Pydantic v2.**
22
+
23
+ ---
24
+
25
+ ## 🛠️ Quick Start
26
+
27
+ ### ![Falcon](https://img.shields.io/badge/Falcon-45b8d8?style=flat&logo=falcon&logoColor=white)
28
+ ```python
29
+ import falcon.asgi
30
+ import uvicorn
31
+ from pydantic import BaseModel
32
+
33
+ from fastopenapi.routers.falcon import FalconRouter
34
+
35
+ app = falcon.asgi.App()
36
+ router = FalconRouter(app=app, docs_url="/docs/", openapi_version="3.0.0")
37
+
38
+
39
+ class HelloResponse(BaseModel):
40
+ message: str
41
+
42
+
43
+ @router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
44
+ async def hello(name: str):
45
+ """Say hello from Falcon"""
46
+ return HelloResponse(message=f"Hello, {name}! It's Falcon!")
47
+
48
+
49
+ if __name__ == "__main__":
50
+ uvicorn.run(app, host="127.0.0.1", port=8000)
51
+
52
+ ```
53
+
54
+ ---
55
+
56
+ ### ![Flask](https://img.shields.io/badge/-Flask-000000?style=flat-square&logo=flask&logoColor=white)
57
+ ```python
58
+ from flask import Flask
59
+ from pydantic import BaseModel
60
+
61
+ from fastopenapi.routers.flask import FlaskRouter
62
+
63
+ app = Flask(__name__)
64
+ router = FlaskRouter(app=app, docs_url="/docs/", openapi_version="3.0.0")
65
+
66
+
67
+ class HelloResponse(BaseModel):
68
+ message: str
69
+
70
+
71
+ @router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
72
+ def hello(name: str):
73
+ """Say hello from Flask"""
74
+ return HelloResponse(message=f"Hello, {name}! It's Flask!")
75
+
76
+
77
+ if __name__ == "__main__":
78
+ app.run(debug=True, port=8000)
79
+
80
+ ```
81
+
82
+ ---
83
+
84
+ ### ![Sanic](https://img.shields.io/badge/-Sanic-00bfff?style=flat-square&logo=sanic&logoColor=white)
85
+ ```python
86
+ from pydantic import BaseModel
87
+ from sanic import Sanic
88
+
89
+ from fastopenapi.routers.sanic import SanicRouter
90
+
91
+ app = Sanic("MySanicApp")
92
+ router = SanicRouter(app=app, docs_url="/docs/", openapi_version="3.0.0")
93
+
94
+
95
+ class HelloResponse(BaseModel):
96
+ message: str
97
+
98
+
99
+ @router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
100
+ async def hello(name: str):
101
+ """Say hello from Sanic"""
102
+ return HelloResponse(message=f"Hello, {name}! It's Sanic!")
103
+
104
+
105
+ if __name__ == "__main__":
106
+ app.run(host="0.0.0.0", port=8000, debug=True)
107
+
108
+ ```
109
+
110
+ ---
111
+
112
+ ### ![Starlette](https://img.shields.io/badge/-Starlette-ff4785?style=flat-square&logo=starlette&logoColor=white)
113
+ ```python
114
+ import uvicorn
115
+ from pydantic import BaseModel
116
+ from starlette.applications import Starlette
117
+
118
+ from fastopenapi.routers.starlette import StarletteRouter
119
+
120
+ app = Starlette()
121
+ router = StarletteRouter(app=app, docs_url="/docs/", openapi_version="3.0.0")
122
+
123
+
124
+ class HelloResponse(BaseModel):
125
+ message: str
126
+
127
+
128
+ @router.get("/hello", tags=["Hello"], status_code=200, response_model=HelloResponse)
129
+ async def hello(name: str):
130
+ """Say hello from Starlette"""
131
+ return HelloResponse(message=f"Hello, {name}! It's Starlette!")
132
+
133
+
134
+ router.register_routes()
135
+
136
+ if __name__ == "__main__":
137
+ uvicorn.run(app, host="127.0.0.1", port=8000)
138
+
139
+ ```
140
+
141
+ ---
142
+
143
+ ## 🛡️ **Type Safety with Pydantic v2**
144
+ ```python
145
+ from pydantic import BaseModel
146
+
147
+ class User(BaseModel):
148
+ id: int
149
+ name: str
150
+
151
+ @router.post("/api/v1/users/")
152
+ def create_user(user: User) -> User:
153
+ return user
154
+ ```
155
+
156
+ ---
157
+
158
+ ## 🧪 **Running Tests**
159
+ ```bash
160
+ poetry run pytest
161
+ ```
162
+
163
+ ---
164
+
165
+ ## 📄 **License**
166
+ This project is licensed under the terms of the MIT license.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,177 @@
1
+ import inspect
2
+ from collections.abc import Callable
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel
6
+
7
+ SWAGGER_URL = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.20.0/"
8
+
9
+
10
+ class BaseRouter:
11
+ def __init__(
12
+ self,
13
+ app: Any = None,
14
+ docs_url: str = "/docs/",
15
+ openapi_version: str = "3.0.0",
16
+ title: str = "My App",
17
+ version: str = "0.1.0",
18
+ ):
19
+ self.app = app
20
+ self.docs_url = docs_url
21
+ self.openapi_version = openapi_version
22
+ self.title = title
23
+ self.version = version
24
+ self._routes: list[tuple[str, str, Callable]] = []
25
+
26
+ def add_route(self, path: str, method: str, endpoint: Callable):
27
+ self._routes.append((path, method.upper(), endpoint))
28
+
29
+ def get_routes(self):
30
+ return self._routes
31
+
32
+ def include_router(self, other: "BaseRouter"):
33
+ self._routes.extend(other.get_routes())
34
+
35
+ def get(self, path: str, **meta):
36
+ def decorator(func: Callable):
37
+ func.__route_meta__ = meta
38
+ self.add_route(path, "GET", func)
39
+ return func
40
+
41
+ return decorator
42
+
43
+ def post(self, path: str, **meta):
44
+ def decorator(func: Callable):
45
+ func.__route_meta__ = meta
46
+ self.add_route(path, "POST", func)
47
+ return func
48
+
49
+ return decorator
50
+
51
+ def put(self, path: str, **meta):
52
+ def decorator(func: Callable):
53
+ func.__route_meta__ = meta
54
+ self.add_route(path, "PUT", func)
55
+ return func
56
+
57
+ return decorator
58
+
59
+ def patch(self, path: str, **meta):
60
+ def decorator(func: Callable):
61
+ func.__route_meta__ = meta
62
+ self.add_route(path, "PATCH", func)
63
+ return func
64
+
65
+ return decorator
66
+
67
+ def delete(self, path: str, **meta):
68
+ def decorator(func: Callable):
69
+ func.__route_meta__ = meta
70
+ self.add_route(path, "DELETE", func)
71
+ return func
72
+
73
+ return decorator
74
+
75
+ def generate_openapi(self) -> dict:
76
+ schema = {
77
+ "openapi": self.openapi_version,
78
+ "info": {"title": self.title, "version": self.version},
79
+ "paths": {},
80
+ "components": {"schemas": {}},
81
+ }
82
+ definitions = {}
83
+
84
+ for path, method, endpoint in self._routes:
85
+ operation = self._build_operation(endpoint, definitions)
86
+ schema["paths"].setdefault(path, {})[method.lower()] = operation
87
+
88
+ schema["components"]["schemas"].update(definitions)
89
+ return schema
90
+
91
+ def _build_operation(self, endpoint, definitions: dict) -> dict:
92
+ sig = inspect.signature(endpoint)
93
+ parameters = []
94
+ request_body = None
95
+
96
+ for param_name, param in sig.parameters.items():
97
+ if param.annotation is inspect.Parameter.empty:
98
+ continue
99
+
100
+ if isinstance(param.annotation, type) and issubclass(
101
+ param.annotation, BaseModel
102
+ ):
103
+ model_schema = self._get_model_schema(param.annotation, definitions)
104
+ request_body = {
105
+ "content": {"application/json": {"schema": model_schema}},
106
+ "required": True,
107
+ }
108
+ else:
109
+ parameters.append(
110
+ {
111
+ "name": param_name,
112
+ "in": "query",
113
+ "required": param.default is inspect.Parameter.empty,
114
+ "schema": {"type": "string"},
115
+ }
116
+ )
117
+
118
+ op = {
119
+ "summary": endpoint.__doc__ or "",
120
+ "responses": {"200": {"description": "OK"}},
121
+ }
122
+ if parameters:
123
+ op["parameters"] = parameters
124
+ if request_body:
125
+ op["requestBody"] = request_body
126
+
127
+ meta = getattr(endpoint, "__route_meta__", {})
128
+ if meta.get("tags"):
129
+ op["tags"] = meta["tags"]
130
+ if meta.get("status_code"):
131
+ code = str(meta["status_code"])
132
+ op["responses"] = {code: {"description": "OK"}}
133
+ response_model = meta.get("response_model")
134
+ if (
135
+ response_model
136
+ and isinstance(response_model, type)
137
+ and issubclass(response_model, BaseModel)
138
+ ):
139
+ resp_model_schema = self._get_model_schema(response_model, definitions)
140
+ op["responses"][code]["content"] = {
141
+ "application/json": {"schema": resp_model_schema}
142
+ }
143
+ return op
144
+
145
+ @staticmethod
146
+ def _get_model_schema(model: type[BaseModel], definitions: dict) -> dict:
147
+ model_schema = model.model_json_schema(
148
+ ref_template="#/components/schemas/{model}"
149
+ )
150
+ for key in ("definitions", "$defs"):
151
+ if key in model_schema:
152
+ definitions.update(model_schema[key])
153
+ del model_schema[key]
154
+ return model_schema
155
+
156
+ @staticmethod
157
+ def render_swagger_ui(openapi_json_url: str) -> str:
158
+ return f"""
159
+ <!DOCTYPE html>
160
+ <html lang="en">
161
+ <head>
162
+ <meta charset="UTF-8">
163
+ <title>Swagger UI</title>
164
+ <link rel="stylesheet" href="{SWAGGER_URL}swagger-ui.css" />
165
+ </head>
166
+ <body>
167
+ <div id="swagger-ui"></div>
168
+ <script src="{SWAGGER_URL}swagger-ui-bundle.js"></script>
169
+ <script>
170
+ SwaggerUIBundle({{
171
+ url: '{openapi_json_url}',
172
+ dom_id: '#swagger-ui'
173
+ }});
174
+ </script>
175
+ </body>
176
+ </html>
177
+ """
File without changes
@@ -0,0 +1,84 @@
1
+ import inspect
2
+ import json
3
+
4
+ import falcon.asgi
5
+ from pydantic import BaseModel
6
+
7
+ from fastopenapi.base_router import BaseRouter
8
+
9
+ METHODS_MAPPER = {
10
+ "GET": "on_get",
11
+ "POST": "on_post",
12
+ "PUT": "on_put",
13
+ "PATCH": "on_patch",
14
+ "DELETE": "on_delete",
15
+ }
16
+
17
+
18
+ class FalconRouter(BaseRouter):
19
+ def __init__(
20
+ self,
21
+ app: falcon.asgi.App = None,
22
+ docs_url: str = "/docs/",
23
+ openapi_version: str = "3.0.0",
24
+ title: str = "My Falcon App",
25
+ version: str = "0.1.0",
26
+ ):
27
+ super().__init__(app, docs_url, openapi_version, title, version)
28
+ if self.app is not None:
29
+ self._register_docs_endpoints()
30
+
31
+ def add_route(self, path: str, method: str, endpoint):
32
+ super().add_route(path, method, endpoint)
33
+ if self.app is not None:
34
+ resource = self._create_resource(endpoint, method.upper())
35
+ self.app.add_route(path, resource)
36
+
37
+ def include_router(self, other: BaseRouter):
38
+ for path, method, endpoint in other.get_routes():
39
+ self.add_route(path, method, endpoint)
40
+
41
+ def _create_resource(self, endpoint, method: str):
42
+ class Resource:
43
+ async def handle(inner_self, req, resp):
44
+ params = dict(req.params)
45
+ try:
46
+ body_bytes = await req.bounded_stream.read()
47
+ if body_bytes:
48
+ body = json.loads(body_bytes.decode("utf-8"))
49
+ params.update(body)
50
+ except Exception:
51
+ pass
52
+ try:
53
+ if inspect.iscoroutinefunction(endpoint):
54
+ result = await endpoint(**params)
55
+ else:
56
+ result = endpoint(**params)
57
+ except TypeError as exc:
58
+ resp.status = falcon.HTTP_422
59
+ resp.media = {"detail": str(exc)}
60
+ return
61
+ if isinstance(result, BaseModel):
62
+ result = result.model_dump()
63
+ resp.media = result
64
+
65
+ res = Resource()
66
+ setattr(res, METHODS_MAPPER[method], res.handle)
67
+ return res
68
+
69
+ def _register_docs_endpoints(self):
70
+ outer = self
71
+
72
+ class OpenAPISchemaResource:
73
+ async def on_get(inner_self, req, resp):
74
+ resp.media = outer.generate_openapi()
75
+
76
+ self.app.add_route("/openapi.json", OpenAPISchemaResource())
77
+
78
+ class SwaggerUIResource:
79
+ async def on_get(inner_self, req, resp):
80
+ html = outer.render_swagger_ui("/openapi.json")
81
+ resp.content_type = "text/html"
82
+ resp.text = html
83
+
84
+ self.app.add_route(self.docs_url, SwaggerUIResource())
@@ -0,0 +1,51 @@
1
+ from flask import Flask, Response, jsonify, request
2
+ from pydantic import BaseModel
3
+
4
+ from fastopenapi.base_router import BaseRouter
5
+
6
+
7
+ class FlaskRouter(BaseRouter):
8
+ def __init__(
9
+ self,
10
+ app: Flask = None,
11
+ docs_url: str = "/docs/",
12
+ openapi_version: str = "3.0.0",
13
+ title: str = "My Flask App",
14
+ version: str = "0.1.0",
15
+ ):
16
+ super().__init__(app, docs_url, openapi_version, title, version)
17
+ if self.app is not None:
18
+ self._register_docs_endpoints()
19
+
20
+ def add_route(self, path: str, method: str, endpoint):
21
+ super().add_route(path, method, endpoint)
22
+ if self.app is not None:
23
+
24
+ def view_func(**kwargs):
25
+ json_data = request.get_json(silent=True) or {}
26
+ params = {**request.args.to_dict(), **json_data}
27
+ try:
28
+ result = endpoint(**params)
29
+ except TypeError as exc:
30
+ return jsonify({"detail": str(exc)}), 422
31
+ if isinstance(result, BaseModel):
32
+ result = result.model_dump()
33
+ return jsonify(result)
34
+
35
+ self.app.add_url_rule(
36
+ path, endpoint.__name__, view_func, methods=[method.upper()]
37
+ )
38
+
39
+ def include_router(self, other: BaseRouter):
40
+ for path, method, endpoint in other.get_routes():
41
+ self.add_route(path, method, endpoint)
42
+
43
+ def _register_docs_endpoints(self):
44
+ @self.app.route("/openapi.json", methods=["GET"])
45
+ def openapi_view():
46
+ return jsonify(self.generate_openapi())
47
+
48
+ @self.app.route(self.docs_url, methods=["GET"])
49
+ def docs_view():
50
+ html = self.render_swagger_ui("/openapi.json")
51
+ return Response(html, mimetype="text/html")
@@ -0,0 +1,57 @@
1
+ import inspect
2
+
3
+ from pydantic import BaseModel
4
+ from sanic import Sanic, response
5
+
6
+ from fastopenapi.base_router import BaseRouter
7
+
8
+
9
+ class SanicRouter(BaseRouter):
10
+ def __init__(
11
+ self,
12
+ app: Sanic = None,
13
+ docs_url: str = "/docs/",
14
+ openapi_version: str = "3.0.0",
15
+ title: str = "My Sanic App",
16
+ version: str = "0.1.0",
17
+ ):
18
+ super().__init__(app, docs_url, openapi_version, title, version)
19
+ if self.app is not None:
20
+ self._register_docs_endpoints()
21
+
22
+ def add_route(self, path: str, method: str, endpoint):
23
+ super().add_route(path, method, endpoint)
24
+
25
+ async def view_func(request, *args, **kwargs):
26
+ params = {
27
+ k: (v[0] if isinstance(v, list) else v) for k, v in request.args.items()
28
+ }
29
+ if request.json:
30
+ params.update(request.json)
31
+ try:
32
+ if inspect.iscoroutinefunction(endpoint):
33
+ result = await endpoint(**params)
34
+ else:
35
+ result = endpoint(**params)
36
+ except TypeError as exc:
37
+ return response.json({"detail": str(exc)}, status=422)
38
+ if isinstance(result, BaseModel):
39
+ result = result.model_dump()
40
+ return response.json(result)
41
+
42
+ route_name = f"{endpoint.__name__}_{method.lower()}_{path.replace('/', '_')}"
43
+ self.app.add_route(view_func, path, methods=[method.upper()], name=route_name)
44
+
45
+ def include_router(self, other: BaseRouter):
46
+ for path, method, endpoint in other.get_routes():
47
+ self.add_route(path, method, endpoint)
48
+
49
+ def _register_docs_endpoints(self):
50
+ @self.app.route("/openapi.json", methods=["GET"])
51
+ async def openapi_view(request):
52
+ return response.json(self.generate_openapi())
53
+
54
+ @self.app.route(self.docs_url, methods=["GET"])
55
+ async def docs_view(request):
56
+ html = self.render_swagger_ui("/openapi.json")
57
+ return response.html(html)
@@ -0,0 +1,71 @@
1
+ import inspect
2
+ import json
3
+
4
+ from pydantic import BaseModel
5
+ from starlette.applications import Starlette
6
+ from starlette.responses import HTMLResponse, JSONResponse
7
+ from starlette.routing import Route
8
+
9
+ from fastopenapi.base_router import BaseRouter
10
+
11
+
12
+ class StarletteRouter(BaseRouter):
13
+ def __init__(
14
+ self,
15
+ app: Starlette = None,
16
+ docs_url: str = "/docs/",
17
+ openapi_version: str = "3.0.0",
18
+ title: str = "My Starlette App",
19
+ version: str = "0.1.0",
20
+ ):
21
+ super().__init__(app, docs_url, openapi_version, title, version)
22
+ self._routes_starlette = []
23
+ if self.app is not None:
24
+ self._register_docs_endpoints()
25
+
26
+ def add_route(self, path: str, method: str, endpoint):
27
+ super().add_route(path, method, endpoint)
28
+ if self.app is not None:
29
+
30
+ async def view(request):
31
+ params = dict(request.query_params)
32
+ try:
33
+ body = await request.body()
34
+ if body:
35
+ json_body = json.loads(body.decode("utf-8"))
36
+ params.update(json_body)
37
+ except Exception:
38
+ pass
39
+ try:
40
+ if inspect.iscoroutinefunction(endpoint):
41
+ result = await endpoint(**params)
42
+ else:
43
+ result = endpoint(**params)
44
+ except TypeError as exc:
45
+ return JSONResponse({"detail": str(exc)}, status_code=422)
46
+ if isinstance(result, BaseModel):
47
+ result = result.model_dump()
48
+ return JSONResponse(result)
49
+
50
+ self._routes_starlette.append(Route(path, view, methods=[method.upper()]))
51
+
52
+ def include_router(self, other: BaseRouter):
53
+ for path, method, endpoint in other.get_routes():
54
+ self.add_route(path, method, endpoint)
55
+
56
+ def register_routes(self):
57
+ if self.app is not None:
58
+ self.app.router.routes.extend(self._routes_starlette)
59
+
60
+ def _register_docs_endpoints(self):
61
+ async def openapi_view(request):
62
+ return JSONResponse(self.generate_openapi())
63
+
64
+ async def docs_view(request):
65
+ html = self.render_swagger_ui("/openapi.json")
66
+ return HTMLResponse(html)
67
+
68
+ self.app.router.routes.append(
69
+ Route("/openapi.json", openapi_view, methods=["GET"])
70
+ )
71
+ self.app.router.routes.append(Route(self.docs_url, docs_view, methods=["GET"]))
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "fastopenapi"
3
+ version = "0.1.0"
4
+ description = "A library for generating and integrating OpenAPI schemas using Pydantic v2."
5
+ authors = [
6
+ {name = "Nikita Ryzhenkov", email = "nikita.ryzhenkoff@gmail.com"}
7
+ ]
8
+ license = {text = "MIT License"}
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ keywords = ["FastOpenAPI", "OpenAPI", "Pydantic", "API", "Python", "Flask", "Falcon", "Sanic", "Starlette"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.10",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Topic :: Software Development :: Libraries",
20
+ "Topic :: Software Development :: Code Generators",
21
+ ]
22
+
23
+ dependencies = [
24
+ "pydantic (>=2.10.6,<3.0.0)"
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ falcon = ["falcon (>=4.0.2,<5.0.0)"]
29
+ flask = ["flask (>=3.1.0,<4.0.0)"]
30
+ sanic = ["sanic (>=24.12.0,<25.0.0)"]
31
+ starlette = ["starlette (>=0.46.0,<0.47.0)"]
32
+
33
+ [build-system]
34
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
35
+ build-backend = "poetry.core.masonry.api"
36
+
37
+ [tool.poetry.group.dev.dependencies]
38
+ mypy = "^1.15.0"
39
+ flake8 = "^7.1.2"
40
+ autoflake = "^2.3.1"
41
+ isort = "^6.0.1"
42
+ black = "^25.1.0"
43
+ pre-commit = "^4.1.0"
44
+ pyupgrade = "^3.19.1"
45
+ pytest = "^8.3.4"
46
+ anyio = "^4.8.0"
47
+ sanic-testing = "^24.6.0"
48
+ trio = "^0.29.0"
49
+ pytest-anyio = "^0.0.0"
50
+ pytest-asyncio = "^0.25.3"
51
+ coverage = "^7.6.12"