fastapi-dynamic-route-registration 0.1.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,14 @@
1
+ """
2
+ FastAPI Dynamic Route Registration
3
+ ------------------------------------
4
+ A library to help dynamically register routes in FastAPI applications.
5
+
6
+ Drop-in spiritual successor to ``flask-dynamic-route-registration``.
7
+ """
8
+
9
+ from .register_routers import register_router, register_routers
10
+ from .router_decorator import register_route
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ __all__ = ["register_route", "register_router", "register_routers"]
File without changes
@@ -0,0 +1,109 @@
1
+ from collections.abc import Mapping
2
+ from inspect import getmembers
3
+ from typing import Any
4
+
5
+ from fastapi import APIRouter, FastAPI
6
+
7
+ from .router_decorator import is_register_route
8
+
9
+
10
+ def register_router(
11
+ container: FastAPI | APIRouter,
12
+ module_name: str,
13
+ routers_data: str | Any | list[str | Any],
14
+ route_kwargs: Mapping[str, Any],
15
+ ) -> list[APIRouter]:
16
+ """Register a single module's routes into *container*.
17
+
18
+ Args:
19
+ container: A ``FastAPI`` app or parent ``APIRouter`` to mount the new
20
+ router(s) into.
21
+ module_name: Dotted import path of the module that contains
22
+ ``@register_route``-decorated functions.
23
+ routers_data: Configuration data.
24
+
25
+ - ``str`` — treated as the URL prefix for a single router.
26
+ - ``dict`` — may have keys ``router_name``, ``router_kwargs``
27
+ (passed to ``APIRouter()``) and ``params`` (injected into every
28
+ route as default values, same semantics as the Flask version).
29
+ - ``list`` — list of the above; mounts the module multiple times
30
+ at different prefixes or with different params.
31
+
32
+ route_kwargs: Extra key/value pairs merged into every router's
33
+ ``params`` dict and bound as default parameter values in decorated
34
+ route functions. Use this to inject shared dependencies such as
35
+ an application context object.
36
+
37
+ Returns:
38
+ List of the newly created ``APIRouter`` instances (empty list if the
39
+ module contains no ``@register_route``-decorated functions).
40
+ """
41
+ module_object = __import__(module_name, fromlist=[""])
42
+ functions = getmembers(module_object, is_register_route)
43
+
44
+ if not functions:
45
+ return []
46
+
47
+ if not isinstance(routers_data, list):
48
+ routers_data = [routers_data]
49
+
50
+ new_routers: list[APIRouter] = []
51
+
52
+ for router_data in routers_data:
53
+ if isinstance(router_data, str):
54
+ router_data = {"router_kwargs": {"prefix": router_data}}
55
+
56
+ router_kwargs: dict[str, Any] = router_data.get("router_kwargs", {})
57
+ params: dict[str, Any] = {**router_data.get("params", {}), **route_kwargs}
58
+
59
+ # Normalise Flask-style url_prefix key in router_kwargs
60
+ if "url_prefix" in router_kwargs and "prefix" not in router_kwargs:
61
+ router_kwargs["prefix"] = router_kwargs.pop("url_prefix")
62
+
63
+ new_router = APIRouter(**router_kwargs)
64
+
65
+ for _name, register_fn in functions:
66
+ register_fn(new_router, **params)
67
+
68
+ container.include_router(new_router)
69
+ new_routers.append(new_router)
70
+
71
+ return new_routers
72
+
73
+
74
+ def register_routers(
75
+ container: FastAPI | APIRouter,
76
+ module_prefix: str,
77
+ routers: Mapping[str, Any],
78
+ route_kwargs: Mapping[str, Any],
79
+ **kwargs: Any,
80
+ ) -> APIRouter:
81
+ """Register multiple modules' routes into *container*.
82
+
83
+ Mirrors ``register_blueprints`` from the Flask version. Each key in
84
+ *routers* is a module suffix (appended to *module_prefix* with a ``.``
85
+ separator) and its value is the *routers_data* accepted by
86
+ :func:`register_router`.
87
+
88
+ Args:
89
+ container: A ``FastAPI`` app or parent ``APIRouter``.
90
+ module_prefix: Common dotted prefix for all modules (e.g.
91
+ ``"myapp.api"``).
92
+ routers: Mapping of ``{module_suffix: routers_data}``.
93
+ route_kwargs: Extra key/value pairs merged into every route's params
94
+ and bound as default parameter values (e.g.
95
+ ``{"app_context": ctx}``).
96
+ **kwargs: Extra kwargs forwarded to the top-level ``APIRouter``
97
+ constructor (e.g. ``tags``, ``dependencies``).
98
+
99
+ Returns:
100
+ The top-level ``APIRouter`` that wraps all sub-routers.
101
+ """
102
+ parent_router = APIRouter(**kwargs)
103
+
104
+ for module_suffix, routers_data in routers.items():
105
+ module_name = f"{module_prefix}.{module_suffix}"
106
+ register_router(parent_router, module_name, routers_data, route_kwargs)
107
+
108
+ container.include_router(parent_router)
109
+ return parent_router
@@ -0,0 +1,139 @@
1
+ import functools
2
+ import inspect
3
+ import types
4
+ from collections.abc import Callable
5
+ from typing import Any, Protocol
6
+
7
+
8
+ class EndpointEnabledCallback(Protocol):
9
+ def __call__(self, **params: Any) -> bool: ...
10
+
11
+
12
+ def _bind_params_as_defaults(
13
+ func: Callable[..., Any], params: dict[str, Any]
14
+ ) -> Callable[..., Any]:
15
+ """Return a new callable with *params* injected as parameter defaults.
16
+
17
+ FastAPI introspects ``__signature__`` to build request validation and
18
+ dependency injection. We replace the signature so that any key in
19
+ *params* that matches a parameter name becomes its default value,
20
+ overriding any pre-existing default. This mirrors the
21
+ ``defaults=params`` mechanism of Flask blueprints.
22
+
23
+ Only params whose keys appear in the function signature (or the function
24
+ accepts ``**kwargs``) are injected; unrecognized keys are silently dropped
25
+ so that one ``route_kwargs`` dict can be broadcast to all routes without
26
+ causing ``TypeError`` for routes that don't declare every key.
27
+
28
+ ``**kwargs`` (VAR_KEYWORD) parameters are stripped from the
29
+ FastAPI-visible signature because FastAPI cannot handle them and would
30
+ attempt to treat them as query parameters.
31
+ """
32
+ sig = inspect.signature(func)
33
+ accepts_var_keyword = any(
34
+ p.kind is inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
35
+ )
36
+ # Params to actually inject: only those accepted by the function.
37
+ injectable = {
38
+ k: v for k, v in params.items() if k in sig.parameters or accepts_var_keyword
39
+ }
40
+
41
+ new_params = []
42
+ for name, param in sig.parameters.items():
43
+ # Drop **kwargs from the FastAPI-visible signature: FastAPI cannot
44
+ # handle VAR_KEYWORD parameters and would try to make them query params.
45
+ if param.kind is inspect.Parameter.VAR_KEYWORD:
46
+ continue
47
+ if name in injectable:
48
+ # Always override the existing default so that injected values
49
+ # from per-router params take precedence over function-level defaults.
50
+ new_params.append(param.replace(default=injectable[name]))
51
+ else:
52
+ new_params.append(param)
53
+
54
+ if inspect.iscoroutinefunction(func):
55
+
56
+ @functools.wraps(func)
57
+ async def async_bound(*args: Any, **kwargs: Any) -> Any:
58
+ for k, v in injectable.items():
59
+ kwargs.setdefault(k, v)
60
+ return await func(*args, **kwargs)
61
+
62
+ async_bound.__signature__ = sig.replace(parameters=new_params) # type: ignore[attr-defined]
63
+ return async_bound
64
+ else:
65
+
66
+ @functools.wraps(func)
67
+ def sync_bound(*args: Any, **kwargs: Any) -> Any:
68
+ for k, v in injectable.items():
69
+ kwargs.setdefault(k, v)
70
+ return func(*args, **kwargs)
71
+
72
+ sync_bound.__signature__ = sig.replace(parameters=new_params) # type: ignore[attr-defined]
73
+ return sync_bound
74
+
75
+
76
+ def register_route(
77
+ path: str,
78
+ *,
79
+ methods: list[str] | None = None,
80
+ endpoint: str | None = None,
81
+ enabled: bool | EndpointEnabledCallback = True,
82
+ **route_kwargs: Any,
83
+ ) -> Callable[..., Any]:
84
+ """Decorator that marks a function as a FastAPI route to be registered later.
85
+
86
+ Usage is intentionally identical to the Flask ``register_route`` decorator so
87
+ that endpoint modules can be ported with minimal diff.
88
+
89
+ Args:
90
+ path: The URL path relative to the router prefix (e.g. ``""`` or
91
+ ``"/{dag_id}"``). Flask ``<param>`` syntax is **not** supported;
92
+ use FastAPI path syntax ``{param}`` directly.
93
+ methods: HTTP methods (default ``["GET"]``).
94
+ endpoint: Override the operation name; defaults to the function name.
95
+ enabled: ``True``, ``False``, or a callable ``(**params) -> bool`` that
96
+ receives the per-router *params* at registration time.
97
+ **route_kwargs: Extra kwargs forwarded to ``router.add_api_route()``.
98
+
99
+ Returns:
100
+ A ``Callable`` wrapper tagged with ``.is_url_rule = True``. When
101
+ called as ``wrapper(router, **params)``, it registers the original
102
+ function on the given ``APIRouter`` with *params* bound as parameter
103
+ defaults via :func:`_bind_params_as_defaults`.
104
+ """
105
+ _methods = methods or ["GET"]
106
+
107
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
108
+ _endpoint = endpoint or func.__name__
109
+
110
+ def wrapper(router: Any, **params: Any) -> None:
111
+ if callable(enabled):
112
+ endpoint_enabled: bool = enabled(**params)
113
+ else:
114
+ endpoint_enabled = enabled
115
+
116
+ if not endpoint_enabled:
117
+ return
118
+
119
+ # Always run through _bind_params_as_defaults so that **kwargs
120
+ # is stripped from the FastAPI-visible signature even when params
121
+ # is empty.
122
+ bound_func = _bind_params_as_defaults(func, params)
123
+
124
+ router.add_api_route(
125
+ path,
126
+ bound_func,
127
+ methods=_methods,
128
+ name=_endpoint,
129
+ **route_kwargs,
130
+ )
131
+
132
+ wrapper.is_url_rule = True # type: ignore[attr-defined]
133
+ return wrapper
134
+
135
+ return decorator
136
+
137
+
138
+ def is_register_route(func: Any) -> bool:
139
+ return isinstance(func, types.FunctionType) and getattr(func, "is_url_rule", False)
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-dynamic-route-registration
3
+ Version: 0.1.0
4
+ Summary: A library to help dynamically register routes in FastAPI applications.
5
+ Author-email: Jérôme DIAZ <jerome_diaz@me.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jeromediaz/fastapi-dynamic-route-registration
8
+ Project-URL: Bug Tracker, https://github.com/jeromediaz/fastapi-dynamic-route-registration/issues
9
+ Keywords: fastapi,route,dynamic
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: fastapi>=0.100.0
19
+ Requires-Dist: typing-extensions>=4.0.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: black; extra == "dev"
22
+ Requires-Dist: pytest; extra == "dev"
23
+ Requires-Dist: ruff; extra == "dev"
24
+ Requires-Dist: mypy; extra == "dev"
25
+ Requires-Dist: httpx; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # FastAPI dynamic route registration
29
+
30
+ [![PyPI - License](https://img.shields.io/pypi/l/fastapi-dynamic-route-registration)](https://pypi.org/project/fastapi-dynamic-route-registration/)
31
+ [![PyPI - Version](https://img.shields.io/pypi/v/fastapi-dynamic-route-registration)](https://pypi.org/project/fastapi-dynamic-route-registration/)
32
+
33
+ A library to help dynamically register routes in FastAPI applications.
34
+
35
+ It is the FastAPI successor to [flask-dynamic-route-registration](https://github.com/jeromediaz/flask-dynamic-route-registration): endpoint modules decorate their functions with `@register_route`, and the application mounts whole modules from a configuration map — with per-router prefixes and shared values injected as parameter defaults.
36
+
37
+ ```bash
38
+ pip install fastapi-dynamic-route-registration
39
+ ```
40
+
41
+ ## How to Use
42
+ ### Basic Setup
43
+
44
+ ```python
45
+ from fastapi import FastAPI
46
+ from fastapi_dynamic_route_registration import register_router
47
+
48
+ app = FastAPI()
49
+
50
+ register_router(app, "healthcheck", "", {})
51
+ ```
52
+
53
+ Inside the `healthcheck/__init__.py` file
54
+
55
+ ```python
56
+ from fastapi_dynamic_route_registration import register_route
57
+
58
+
59
+ @register_route("/health")
60
+ def health() -> dict:
61
+ return {"status": "OK"}
62
+ ```
63
+
64
+ ### Reusing a module with different parameters, conditional routes
65
+
66
+ ```python
67
+ api_versions = [
68
+ {"router_kwargs": {"prefix": "/api/1"}, "params": {"version": 1}},
69
+ {"router_kwargs": {"prefix": "/api/2"}, "params": {"version": 2}},
70
+ ]
71
+
72
+ register_router(app, "api", api_versions, {})
73
+ ```
74
+
75
+ Inside the `api/__init__.py` file
76
+
77
+ ```python
78
+ from fastapi_dynamic_route_registration import register_route
79
+
80
+
81
+ @register_route("/status")
82
+ def status(*, version: int = 1) -> dict:
83
+ return {"status": "OK", "version": version}
84
+
85
+
86
+ @register_route("/foo", enabled=lambda **params: params.get("version", 1) >= 2)
87
+ def foo_a() -> dict:
88
+ return {"hello": "world"}
89
+
90
+
91
+ @register_route("/foo/{subject}", enabled=lambda **params: params.get("version", 1) >= 2)
92
+ def foo_b(subject: str) -> dict:
93
+ return {"hello": subject}
94
+ ```
95
+
96
+ This registers four routes:
97
+
98
+ | URL | Return value | Function |
99
+ |----------------------|--------------------------------|----------|
100
+ | /api/1/status | {"status": "OK", "version": 1} | status |
101
+ | /api/2/status | {"status": "OK", "version": 2} | status |
102
+ | /api/2/foo | {"hello": "world"} | foo_a |
103
+ | /api/2/foo/{subject} | {"hello": subject} | foo_b |
104
+
105
+ Values from `params` (and from the `route_kwargs` passed to `register_router`) become the **default value** of the parameter with the same name. FastAPI still sees that parameter: a plain `version: int = 1` stays a query parameter, whose default is now the injected value. Keys a function does not declare are dropped — unless it accepts `**kwargs`, in which case they are passed on every call while `**kwargs` itself stays hidden from FastAPI. One shared mapping (e.g. an application context object) can therefore be broadcast to every route.
106
+
107
+ ### Other route options
108
+
109
+ `register_route` forwards extra keyword arguments to `APIRouter.add_api_route`:
110
+
111
+ ```python
112
+ @register_route("/items", methods=["POST"], status_code=201, tags=["items"])
113
+ def create_item(item: Item) -> Item:
114
+ return item
115
+ ```
116
+
117
+ ### Registering multiple modules at once
118
+
119
+ `register_routers` (plural) registers several modules under a shared parent router:
120
+
121
+ ```python
122
+ from fastapi import FastAPI
123
+ from fastapi_dynamic_route_registration import register_routers
124
+
125
+ app = FastAPI()
126
+
127
+ register_routers(
128
+ app,
129
+ "myapp.api",
130
+ {
131
+ "healthcheck": "",
132
+ "users": {"router_kwargs": {"prefix": "/users"}},
133
+ },
134
+ route_kwargs={},
135
+ prefix="/v1",
136
+ )
137
+ ```
138
+
139
+ Each value accepts the same shapes as `register_router`: a prefix string, a dict with `router_kwargs` (passed to `APIRouter`) and `params`, or a list of those. A configuration map like this is easy to keep in a JSON file per application.
140
+
141
+ ### Differences from the Flask version
142
+
143
+ - Path parameters use FastAPI syntax: `{param}`, not `<param>`.
144
+ - `router_kwargs` replaces `blueprint_kwargs`; a Flask-style `url_prefix` key is still accepted and mapped to `prefix`.
145
+ - One function registers one route.
146
+
147
+ ## License
148
+
149
+ This project is licensed under the MIT License - see the [LICENSE](https://github.com/jeromediaz/fastapi-dynamic-route-registration/blob/main/LICENSE) file for details.
@@ -0,0 +1,9 @@
1
+ fastapi_dynamic_route_registration/__init__.py,sha256=yQnd639AlyfLywnsCg2N4zO0NZCdvT3CcO_TnVB8JLs,423
2
+ fastapi_dynamic_route_registration/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ fastapi_dynamic_route_registration/register_routers.py,sha256=p9Nb1KtZ1wlwks0p-44A1aqBQzduBr1XdmDOdYcvpBg,4055
4
+ fastapi_dynamic_route_registration/router_decorator.py,sha256=mPRpQDLDGftH_DUvHCrZpc0LCWqv5KALaSH0QSzohwA,5340
5
+ fastapi_dynamic_route_registration-0.1.0.dist-info/licenses/LICENSE,sha256=5LFZRGEYJM0yj60bF-rPdi3ANN0-Vb_SOx6PgXS_KLs,1069
6
+ fastapi_dynamic_route_registration-0.1.0.dist-info/METADATA,sha256=_XN1-7efg3kuHP2WuhgjT8ckZrR67IUWqEurnZ55UcU,5419
7
+ fastapi_dynamic_route_registration-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
8
+ fastapi_dynamic_route_registration-0.1.0.dist-info/top_level.txt,sha256=SqDVD53KRr-11bHU-4JdweXVzr8XpZigin5Soa0Qct0,35
9
+ fastapi_dynamic_route_registration-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DIAZ Jérôme
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 @@
1
+ fastapi_dynamic_route_registration