generate-fastapi-typed-routes 0.1.1__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,57 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: generate-fastapi-typed-routes
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Generate typed url_path_for functions for FastAPI applications.
|
|
5
|
+
Keywords: fastapi,typing,routes,codegen
|
|
6
|
+
Author: Michael Bianco
|
|
7
|
+
Author-email: Michael Bianco <mike@mikebian.co>
|
|
8
|
+
Requires-Dist: click>=8.3.1
|
|
9
|
+
Requires-Dist: fastapi>=0.128.0
|
|
10
|
+
Requires-Dist: jinja2>=3.1.6
|
|
11
|
+
Requires-Dist: pydantic>=2.12.5
|
|
12
|
+
Requires-Dist: structlog-config>=0.6.0
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Project-URL: Repository, https://github.com/iloveitaly/generate-fastapi-typed-routes
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Type-Safe Route URL Generator for FastAPI
|
|
18
|
+
|
|
19
|
+
Stop guessing route names. This tool analyzes your FastAPI application and generates a typed wrapper for `url_path_for`, giving you instant autocompletion and catching typos before they hit production.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uvx generate-fastapi-typed-routes@latest
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Point the tool at your FastAPI app and tell it where to save the generated code:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uvx generate-fastapi-typed-routes@latest --app-module myapp.main:app --output myapp/routes.py
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Now, instead of using the raw `app.url_path_for`, import your generated function:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from myapp.routes import app_url_path_for
|
|
39
|
+
|
|
40
|
+
# Complete with IDE autocompletion and type checking!
|
|
41
|
+
url = app_url_path_for("get_user_profile", user_id=123)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### CLI Arguments
|
|
45
|
+
|
|
46
|
+
* `--app-module`: The import path to your FastAPI app instance (e.g., `src.main:app`). You can pass this multiple times to generate helpers for multiple apps in one file.
|
|
47
|
+
* `--output`: The file path where the generated Python code will be saved.
|
|
48
|
+
* `--prefix`: (Optional) Custom prefix for the generated function. Defaults to the app variable name (e.g., `app` becomes `app_url_path_for`). Use this if you have multiple apps to keep things distinct.
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
* **Zero Runtime Overhead:** The generated code is just type hints and a simple wrapper.
|
|
53
|
+
* **IDE Autocompletion:** Never type a route name manually again. Your editor will list every available route name defined in your app.
|
|
54
|
+
* **Refactoring Safe:** Change a route name in your app, and your type checker (mypy, pyright) will flag every place usage that needs updating.
|
|
55
|
+
* **Multi-App Support:** Easily manage routes for projects with multiple FastAPI instances.
|
|
56
|
+
|
|
57
|
+
# [MIT License](LICENSE.md)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Type-Safe Route URL Generator for FastAPI
|
|
2
|
+
|
|
3
|
+
Stop guessing route names. This tool analyzes your FastAPI application and generates a typed wrapper for `url_path_for`, giving you instant autocompletion and catching typos before they hit production.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uvx generate-fastapi-typed-routes@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Point the tool at your FastAPI app and tell it where to save the generated code:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uvx generate-fastapi-typed-routes@latest --app-module myapp.main:app --output myapp/routes.py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Now, instead of using the raw `app.url_path_for`, import your generated function:
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from myapp.routes import app_url_path_for
|
|
23
|
+
|
|
24
|
+
# Complete with IDE autocompletion and type checking!
|
|
25
|
+
url = app_url_path_for("get_user_profile", user_id=123)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### CLI Arguments
|
|
29
|
+
|
|
30
|
+
* `--app-module`: The import path to your FastAPI app instance (e.g., `src.main:app`). You can pass this multiple times to generate helpers for multiple apps in one file.
|
|
31
|
+
* `--output`: The file path where the generated Python code will be saved.
|
|
32
|
+
* `--prefix`: (Optional) Custom prefix for the generated function. Defaults to the app variable name (e.g., `app` becomes `app_url_path_for`). Use this if you have multiple apps to keep things distinct.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
* **Zero Runtime Overhead:** The generated code is just type hints and a simple wrapper.
|
|
37
|
+
* **IDE Autocompletion:** Never type a route name manually again. Your editor will list every available route name defined in your app.
|
|
38
|
+
* **Refactoring Safe:** Change a route name in your app, and your type checker (mypy, pyright) will flag every place usage that needs updating.
|
|
39
|
+
* **Multi-App Support:** Easily manage routes for projects with multiple FastAPI instances.
|
|
40
|
+
|
|
41
|
+
# [MIT License](LICENSE.md)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from types import ModuleType
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from fastapi.routing import APIRoute
|
|
7
|
+
from jinja2 import Template
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from structlog_config import configure_logger
|
|
10
|
+
|
|
11
|
+
log = configure_logger()
|
|
12
|
+
|
|
13
|
+
MODULE_TEMPLATE = '''\
|
|
14
|
+
"""Auto-generated typed url_path_for functions for FastAPI apps."""
|
|
15
|
+
|
|
16
|
+
from typing import overload, Literal
|
|
17
|
+
{% for app_info in apps %}
|
|
18
|
+
from {{ app_info.import_path }} import {{ app_info.name }}
|
|
19
|
+
{% endfor %}
|
|
20
|
+
|
|
21
|
+
{% for app_info in apps %}
|
|
22
|
+
# Routes for {{ app_info.name }}
|
|
23
|
+
{% for route in app_info.routes %}
|
|
24
|
+
@overload
|
|
25
|
+
def {{ app_info.prefix }}_url_path_for(name: Literal["{{ route.name }}"], **path_params) -> str: ...
|
|
26
|
+
{% endfor %}
|
|
27
|
+
|
|
28
|
+
def {{ app_info.prefix }}_url_path_for(name: str, **path_params) -> str:
|
|
29
|
+
"""Type-safe wrapper around {{ app_info.name }}.url_path_for() with overloads for all routes."""
|
|
30
|
+
return {{ app_info.name }}.url_path_for(name, **path_params)
|
|
31
|
+
|
|
32
|
+
{% endfor %}
|
|
33
|
+
'''
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RouteInfo(BaseModel):
|
|
37
|
+
name: str
|
|
38
|
+
path: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AppInfo(BaseModel):
|
|
42
|
+
import_path: str
|
|
43
|
+
name: str
|
|
44
|
+
prefix: str
|
|
45
|
+
routes: list[RouteInfo]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def extract_routes(app: FastAPI) -> list[RouteInfo]:
|
|
49
|
+
"""Extract route information from a FastAPI app."""
|
|
50
|
+
routes: list[RouteInfo] = []
|
|
51
|
+
|
|
52
|
+
for route in app.routes:
|
|
53
|
+
if not isinstance(route, APIRoute):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
if not route.name:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
routes.append(
|
|
60
|
+
RouteInfo(
|
|
61
|
+
name=route.name,
|
|
62
|
+
path=route.path,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Sort for consistent output
|
|
67
|
+
routes.sort(key=lambda x: x.name)
|
|
68
|
+
|
|
69
|
+
log.info("extracted_routes", count=len(routes))
|
|
70
|
+
return routes
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def load_app(app_module: str, prefix: str | None) -> AppInfo:
|
|
74
|
+
"""Load a FastAPI app and extract its information."""
|
|
75
|
+
module_path, app_name = app_module.split(":")
|
|
76
|
+
module: ModuleType = __import__(module_path, fromlist=[app_name])
|
|
77
|
+
app = getattr(module, app_name)
|
|
78
|
+
|
|
79
|
+
if not isinstance(app, FastAPI):
|
|
80
|
+
raise click.ClickException(f"{app_module} is not a FastAPI app")
|
|
81
|
+
|
|
82
|
+
log.info("app_loaded", module=module_path, app=app_name)
|
|
83
|
+
|
|
84
|
+
routes = extract_routes(app)
|
|
85
|
+
|
|
86
|
+
# Determine prefix - default to app_name if not provided
|
|
87
|
+
if prefix is None:
|
|
88
|
+
prefix = app_name
|
|
89
|
+
log.info("using_default_prefix", prefix=prefix)
|
|
90
|
+
|
|
91
|
+
return AppInfo(
|
|
92
|
+
import_path=module_path,
|
|
93
|
+
name=app_name,
|
|
94
|
+
prefix=prefix,
|
|
95
|
+
routes=routes,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def generate_typed_module(apps_info: list[AppInfo], output_path: Path) -> None:
|
|
100
|
+
"""Generate Python module with typed url_path_for functions."""
|
|
101
|
+
|
|
102
|
+
log.info(
|
|
103
|
+
"generating_module", output_path=str(output_path), app_count=len(apps_info)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Render the template
|
|
107
|
+
template = Template(MODULE_TEMPLATE)
|
|
108
|
+
output = template.render(apps=apps_info)
|
|
109
|
+
|
|
110
|
+
# Create parent directories if needed
|
|
111
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
|
|
113
|
+
output_path.write_text(output)
|
|
114
|
+
|
|
115
|
+
total_routes = sum(len(app.routes) for app in apps_info)
|
|
116
|
+
log.info(
|
|
117
|
+
"module_generated", output_path=str(output_path), total_routes=total_routes
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@click.command()
|
|
122
|
+
@click.option(
|
|
123
|
+
"--app-module",
|
|
124
|
+
multiple=True,
|
|
125
|
+
required=True,
|
|
126
|
+
help="Python module path to FastAPI app (e.g., 'myapp.main:api_app'). Can be specified multiple times.",
|
|
127
|
+
)
|
|
128
|
+
@click.option(
|
|
129
|
+
"--output",
|
|
130
|
+
type=click.Path(path_type=Path),
|
|
131
|
+
required=True,
|
|
132
|
+
help="Output path for the generated module (required)",
|
|
133
|
+
)
|
|
134
|
+
@click.option(
|
|
135
|
+
"--prefix",
|
|
136
|
+
multiple=True,
|
|
137
|
+
default=None,
|
|
138
|
+
help="Prefix for the generated function (default: uses app variable name). Should match order of --app-module.",
|
|
139
|
+
)
|
|
140
|
+
def main(
|
|
141
|
+
app_module: tuple[str, ...], output: Path, prefix: tuple[str, ...] | None
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Generate typed url_path_for functions for FastAPI applications."""
|
|
144
|
+
|
|
145
|
+
log.info("starting_generation", app_modules=app_module, output=str(output))
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
# Parse prefixes - if provided, must match number of apps
|
|
149
|
+
prefixes = list(prefix) if prefix else [None] * len(app_module)
|
|
150
|
+
|
|
151
|
+
if len(prefixes) != len(app_module):
|
|
152
|
+
raise click.ClickException(
|
|
153
|
+
f"Number of prefixes ({len(prefixes)}) must match number of app modules ({len(app_module)})"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Load all apps
|
|
157
|
+
apps_info = []
|
|
158
|
+
for app_mod, app_prefix in zip(app_module, prefixes):
|
|
159
|
+
app_info = load_app(app_mod, app_prefix)
|
|
160
|
+
apps_info.append(app_info)
|
|
161
|
+
|
|
162
|
+
# Generate module
|
|
163
|
+
generate_typed_module(apps_info, output)
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
log.error("generation_failed", error=str(e), exc_info=True)
|
|
167
|
+
raise click.ClickException(str(e))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "generate-fastapi-typed-routes"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Generate typed url_path_for functions for FastAPI applications."
|
|
5
|
+
keywords = ["fastapi", "typing", "routes", "codegen"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"click>=8.3.1",
|
|
10
|
+
"fastapi>=0.128.0",
|
|
11
|
+
"jinja2>=3.1.6",
|
|
12
|
+
"pydantic>=2.12.5",
|
|
13
|
+
"structlog-config>=0.6.0",
|
|
14
|
+
]
|
|
15
|
+
authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
|
|
16
|
+
urls = { "Repository" = "https://github.com/iloveitaly/generate-fastapi-typed-routes" }
|
|
17
|
+
|
|
18
|
+
# additional packaging information: https://packaging.python.org/en/latest/specifications/core-metadata/#license
|
|
19
|
+
[project.scripts]
|
|
20
|
+
generate-fastapi-typed-routes = "generate_fastapi_typed_routes:main"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.9.0,<0.10.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
25
|
+
|
|
26
|
+
[tool.uv.build-backend]
|
|
27
|
+
# avoids the src/ directory structure
|
|
28
|
+
module-root = ""
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = ["pytest>=8.3.3", "pyright[nodejs]>=1.1.380", "ruff>=0.8.0"]
|
|
32
|
+
|
|
33
|
+
[tool.pyright]
|
|
34
|
+
exclude = ["examples/", "playground/", "tmp/", ".venv/", "tests/"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
extend-exclude = ["playground.py", "playground/"]
|