fastapi-filebased-routing 1.0.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.
- fastapi_filebased_routing-1.0.0/.github/workflows/ci.yml +50 -0
- fastapi_filebased_routing-1.0.0/.github/workflows/publish.yml +31 -0
- fastapi_filebased_routing-1.0.0/.gitignore +29 -0
- fastapi_filebased_routing-1.0.0/.pre-commit-config.yaml +24 -0
- fastapi_filebased_routing-1.0.0/CHANGELOG.md +61 -0
- fastapi_filebased_routing-1.0.0/LICENSE +21 -0
- fastapi_filebased_routing-1.0.0/PKG-INFO +359 -0
- fastapi_filebased_routing-1.0.0/README.md +326 -0
- fastapi_filebased_routing-1.0.0/examples/advanced/app/(admin)/settings/route.py +40 -0
- fastapi_filebased_routing-1.0.0/examples/advanced/app/api/[[version]]/users/route.py +43 -0
- fastapi_filebased_routing-1.0.0/examples/advanced/app/events/route.py +64 -0
- fastapi_filebased_routing-1.0.0/examples/advanced/app/files/[...path]/route.py +45 -0
- fastapi_filebased_routing-1.0.0/examples/advanced/app/ws/chat/[room_id]/route.py +49 -0
- fastapi_filebased_routing-1.0.0/examples/advanced/main.py +58 -0
- fastapi_filebased_routing-1.0.0/examples/basic/app/health/route.py +9 -0
- fastapi_filebased_routing-1.0.0/examples/basic/app/users/[user_id]/route.py +39 -0
- fastapi_filebased_routing-1.0.0/examples/basic/app/users/route.py +42 -0
- fastapi_filebased_routing-1.0.0/examples/basic/main.py +24 -0
- fastapi_filebased_routing-1.0.0/examples/middleware/app/(admin)/_middleware.py +10 -0
- fastapi_filebased_routing-1.0.0/examples/middleware/app/(admin)/settings/route.py +8 -0
- fastapi_filebased_routing-1.0.0/examples/middleware/app/_middleware.py +9 -0
- fastapi_filebased_routing-1.0.0/examples/middleware/app/api/_middleware.py +11 -0
- fastapi_filebased_routing-1.0.0/examples/middleware/app/api/users/[user_id]/route.py +14 -0
- fastapi_filebased_routing-1.0.0/examples/middleware/app/api/users/route.py +15 -0
- fastapi_filebased_routing-1.0.0/examples/middleware/app/health/route.py +3 -0
- fastapi_filebased_routing-1.0.0/examples/middleware/main.py +9 -0
- fastapi_filebased_routing-1.0.0/pyproject.toml +95 -0
- fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/__init__.py +44 -0
- fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/core/__init__.py +5 -0
- fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/core/importer.py +420 -0
- fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/core/middleware.py +222 -0
- fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/core/parser.py +197 -0
- fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/core/scanner.py +283 -0
- fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/exceptions.py +91 -0
- fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/fastapi/__init__.py +5 -0
- fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/fastapi/router.py +496 -0
- fastapi_filebased_routing-1.0.0/src/fastapi_filebased_routing/py.typed +0 -0
- fastapi_filebased_routing-1.0.0/tests/conftest.py +132 -0
- fastapi_filebased_routing-1.0.0/tests/integration/__init__.py +1 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/__init__.py +0 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/conftest.py +58 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/main.py +27 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/_middleware.py +11 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/_middleware.py +6 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/items/[item_id]/route.py +26 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/messages/route.py +35 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/protected/_middleware.py +19 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/protected/route.py +19 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/tasks/[task_id]/route.py +28 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/api/users/route.py +23 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/fixtures/app/routes/echo/route.py +15 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/test_auth_isolation.py +124 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/test_body_isolation.py +54 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/test_error_isolation.py +118 -0
- fastapi_filebased_routing-1.0.0/tests/integration/concurrency/test_request_isolation.py +173 -0
- fastapi_filebased_routing-1.0.0/tests/integration/test_error_scenarios.py +730 -0
- fastapi_filebased_routing-1.0.0/tests/integration/test_full_routing.py +1303 -0
- fastapi_filebased_routing-1.0.0/tests/integration/test_middleware_errors.py +617 -0
- fastapi_filebased_routing-1.0.0/tests/integration/test_middleware_integration.py +907 -0
- fastapi_filebased_routing-1.0.0/tests/integration/test_security.py +621 -0
- fastapi_filebased_routing-1.0.0/tests/test_conftest_fixtures.py +109 -0
- fastapi_filebased_routing-1.0.0/tests/unit/__init__.py +1 -0
- fastapi_filebased_routing-1.0.0/tests/unit/core/__init__.py +1 -0
- fastapi_filebased_routing-1.0.0/tests/unit/core/test_importer.py +1111 -0
- fastapi_filebased_routing-1.0.0/tests/unit/core/test_middleware.py +454 -0
- fastapi_filebased_routing-1.0.0/tests/unit/core/test_middleware_chain.py +335 -0
- fastapi_filebased_routing-1.0.0/tests/unit/core/test_parser.py +331 -0
- fastapi_filebased_routing-1.0.0/tests/unit/core/test_scanner.py +532 -0
- fastapi_filebased_routing-1.0.0/tests/unit/core/test_smoke.py +24 -0
- fastapi_filebased_routing-1.0.0/tests/unit/fastapi/__init__.py +1 -0
- fastapi_filebased_routing-1.0.0/tests/unit/fastapi/test_router.py +1430 -0
- fastapi_filebased_routing-1.0.0/tests/unit/test_exceptions.py +295 -0
- fastapi_filebased_routing-1.0.0/tests/unit/test_public_api.py +149 -0
- fastapi_filebased_routing-1.0.0/uv.lock +616 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.13", "3.14"]
|
|
14
|
+
name: Test (Python ${{ matrix.python }})
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
|
|
20
|
+
- name: Install Python
|
|
21
|
+
run: uv python install ${{ matrix.python }}
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: uv sync --extra dev
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: uv run pytest --tb=short -q
|
|
28
|
+
|
|
29
|
+
lint:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
name: Lint & Types
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
|
|
35
|
+
- uses: astral-sh/setup-uv@v5
|
|
36
|
+
|
|
37
|
+
- name: Install Python
|
|
38
|
+
run: uv python install 3.13
|
|
39
|
+
|
|
40
|
+
- name: Install dependencies
|
|
41
|
+
run: uv sync --extra dev
|
|
42
|
+
|
|
43
|
+
- name: Ruff check
|
|
44
|
+
run: uv run ruff check src/ tests/
|
|
45
|
+
|
|
46
|
+
- name: Ruff format
|
|
47
|
+
run: uv run ruff format --check src/ tests/
|
|
48
|
+
|
|
49
|
+
- name: Mypy
|
|
50
|
+
run: uv run mypy src/
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
name: Build & Publish
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
contents: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
|
|
19
|
+
- name: Install Python
|
|
20
|
+
run: uv python install 3.13
|
|
21
|
+
|
|
22
|
+
- name: Build package
|
|
23
|
+
run: uv build
|
|
24
|
+
|
|
25
|
+
- name: Publish to PyPI
|
|
26
|
+
run: uv publish --trusted-publishing always
|
|
27
|
+
|
|
28
|
+
- name: Create GitHub Release
|
|
29
|
+
run: gh release create ${{ github.ref_name }} dist/* --generate-notes
|
|
30
|
+
env:
|
|
31
|
+
GH_TOKEN: ${{ github.token }}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.so
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv/
|
|
12
|
+
|
|
13
|
+
# Testing
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
htmlcov/
|
|
17
|
+
|
|
18
|
+
# Type checking
|
|
19
|
+
.mypy_cache/
|
|
20
|
+
|
|
21
|
+
# Ruff
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
|
|
24
|
+
# IDE
|
|
25
|
+
.vscode/
|
|
26
|
+
.idea/
|
|
27
|
+
|
|
28
|
+
# OS
|
|
29
|
+
.DS_Store
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: ruff-check
|
|
5
|
+
name: ruff check
|
|
6
|
+
entry: uv run ruff check --fix
|
|
7
|
+
language: system
|
|
8
|
+
types: [python]
|
|
9
|
+
files: ^(src|tests)/
|
|
10
|
+
|
|
11
|
+
- id: ruff-format
|
|
12
|
+
name: ruff format
|
|
13
|
+
entry: uv run ruff format
|
|
14
|
+
language: system
|
|
15
|
+
types: [python]
|
|
16
|
+
files: ^(src|tests)/
|
|
17
|
+
|
|
18
|
+
- id: mypy
|
|
19
|
+
name: mypy
|
|
20
|
+
entry: uv run mypy src/
|
|
21
|
+
language: system
|
|
22
|
+
types: [python]
|
|
23
|
+
files: ^src/
|
|
24
|
+
pass_filenames: false
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2026-02-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Directory-level middleware** via `_middleware.py` files — applies to all routes in the directory subtree
|
|
13
|
+
- **File-level middleware** via `middleware = [...]` at module level in `route.py` — applies to all handlers in the file
|
|
14
|
+
- **Handler-level middleware** via `class handler(route):` syntax with `middleware = [...]` — per-handler configuration
|
|
15
|
+
- **Three-layer middleware execution order**: directory (parent→child) → file → handler
|
|
16
|
+
- **`route` base class** with metaclass that returns `RouteConfig` — `class get(route):` returns callable config, not a class
|
|
17
|
+
- **`RouteConfig` dataclass** — carries handler, middleware, and metadata (tags, summary, deprecated, status_code)
|
|
18
|
+
- **`build_middleware_chain()`** — composable middleware chain with `call_next` semantics
|
|
19
|
+
- **`_make_middleware_route()`** — custom APIRoute subclass preserving FastAPI dependency injection
|
|
20
|
+
- **`MiddlewareValidationError`** exception for middleware configuration issues
|
|
21
|
+
- **Middleware context enrichment** — middleware can set `request.state` values visible to subsequent middleware and handlers
|
|
22
|
+
- **Middleware short-circuit** — middleware can return response without calling `call_next`
|
|
23
|
+
- **Startup-time validation** — all middleware validated when `create_router_from_path()` is called
|
|
24
|
+
- **Handler-level metadata override** — `class handler(route):` can set `tags`, `summary`, `deprecated`, `status_code`
|
|
25
|
+
- `examples/middleware/` — comprehensive example showing all middleware features
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Bumped version to 0.2.0
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- N/A
|
|
34
|
+
|
|
35
|
+
## [0.1.0] - 2025-10-01
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- Automatic route discovery from directory structure with `create_router_from_path()`
|
|
40
|
+
- HTTP method handlers: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
|
|
41
|
+
- WebSocket handler support via `async def websocket(ws: WebSocket)` convention
|
|
42
|
+
- Dynamic path parameters using `[param]` directory syntax
|
|
43
|
+
- Optional path parameters using `[[param]]` directory syntax with automatic variant generation
|
|
44
|
+
- Catch-all path parameters using `[...param]` directory syntax for arbitrary nested paths
|
|
45
|
+
- Route groups using `(group)` directory syntax for code organization without URL impact
|
|
46
|
+
- Convention-based status codes (POST→201, DELETE→204, others→200)
|
|
47
|
+
- Route metadata support: TAGS, SUMMARY, DEPRECATED constants in route files
|
|
48
|
+
- Automatic tag derivation from path segments (skipping api/version prefixes)
|
|
49
|
+
- Security validation for path traversal attempts
|
|
50
|
+
- Symlink validation to prevent directory traversal attacks
|
|
51
|
+
- Parameter name validation (must be valid Python identifiers)
|
|
52
|
+
- Duplicate route detection with detailed error reporting
|
|
53
|
+
- Sync and async handler support (both `def` and `async def`)
|
|
54
|
+
- Full FastAPI APIRouter integration for seamless coexistence with manual routes
|
|
55
|
+
- Router prefix support via `prefix` parameter
|
|
56
|
+
- Type-safe with PEP 561 marker
|
|
57
|
+
- 98%+ test coverage across all modules
|
|
58
|
+
|
|
59
|
+
[Unreleased]: https://github.com/irudi/fastapi-filebased-routing/compare/v0.2.0...main
|
|
60
|
+
[0.2.0]: https://github.com/irudi/fastapi-filebased-routing/releases/tag/v0.2.0
|
|
61
|
+
[0.1.0]: https://github.com/irudi/fastapi-filebased-routing/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 irudi
|
|
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,359 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-filebased-routing
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Next.js-style file-based routing for FastAPI
|
|
5
|
+
Project-URL: Homepage, https://github.com/irudi/fastapi-filebased-routing
|
|
6
|
+
Project-URL: Documentation, https://github.com/irudi/fastapi-filebased-routing#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/irudi/fastapi-filebased-routing
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/irudi/fastapi-filebased-routing/issues
|
|
9
|
+
Author: irudi
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: fastapi,file-based,filesystem,nextjs,routing
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: FastAPI
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.13
|
|
23
|
+
Requires-Dist: fastapi>=0.115.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
27
|
+
Requires-Dist: pre-commit>=4.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# fastapi-filebased-routing
|
|
35
|
+
|
|
36
|
+
Next.js-style file-based routing for FastAPI
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/fastapi-filebased-routing/)
|
|
39
|
+
[](https://pypi.org/project/fastapi-filebased-routing/)
|
|
40
|
+
[](https://opensource.org/licenses/MIT)
|
|
41
|
+
|
|
42
|
+
Define your API routes through directory structure and convention, not manual registration. Create a `route.py` file in a directory and it becomes an endpoint automatically. Say goodbye to router boilerplate and route registration conflicts.
|
|
43
|
+
|
|
44
|
+
## Table of Contents
|
|
45
|
+
|
|
46
|
+
- [🚀 Installation & Quickstart](#-installation--quickstart)
|
|
47
|
+
- [📁 File-Based Routing Explained](#-file-based-routing-explained)
|
|
48
|
+
- [📍 Route Handlers](#-route-handlers)
|
|
49
|
+
- [🔗 Middleware](#-middleware)
|
|
50
|
+
- [📦 Examples](#-examples)
|
|
51
|
+
- [📖 API Reference](#-api-reference)
|
|
52
|
+
- [💡 Why This Plugin?](#-why-this-plugin)
|
|
53
|
+
- [🤝 Contributing](#-contributing)
|
|
54
|
+
|
|
55
|
+
## 🚀 Installation & Quickstart
|
|
56
|
+
|
|
57
|
+
Requires **Python 3.13+** and **FastAPI 0.115.0+**.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install fastapi-filebased-routing
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from fastapi import FastAPI
|
|
65
|
+
from fastapi_filebased_routing import create_router_from_path
|
|
66
|
+
|
|
67
|
+
app = FastAPI()
|
|
68
|
+
app.include_router(create_router_from_path("app"))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
That's it. Every `route.py` file in your `app/` directory is automatically discovered and registered.
|
|
72
|
+
|
|
73
|
+
## 📁 File-Based Routing Explained
|
|
74
|
+
|
|
75
|
+
Your directory structure defines your URL routes. Given `create_router_from_path("app")`:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
app/
|
|
79
|
+
├── _middleware.py # directory middleware → ALL routes
|
|
80
|
+
├── health/
|
|
81
|
+
│ └── route.py # get → /health
|
|
82
|
+
├── api/
|
|
83
|
+
│ ├── _middleware.py # directory middleware → /api/**
|
|
84
|
+
│ ├── [[version]]/
|
|
85
|
+
│ │ └── route.py # → /api and /api/{version}
|
|
86
|
+
│ └── users/
|
|
87
|
+
│ ├── route.py # file-level middleware + handlers
|
|
88
|
+
│ └── [user_id]/
|
|
89
|
+
│ └── route.py # handler-level middleware via class
|
|
90
|
+
├── files/
|
|
91
|
+
│ └── [...path]/
|
|
92
|
+
│ └── route.py # catch-all route
|
|
93
|
+
├── ws/
|
|
94
|
+
│ └── chat/
|
|
95
|
+
│ └── route.py # websocket handler
|
|
96
|
+
└── (admin)/ # group: excluded from URL
|
|
97
|
+
├── _middleware.py # directory middleware → /settings/**
|
|
98
|
+
└── settings/
|
|
99
|
+
└── route.py # → /settings
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Each `route.py` exports [route handlers](#-route-handlers). Each `_middleware.py` defines [directory middleware](#directory-level-middleware).
|
|
103
|
+
|
|
104
|
+
### Route Conventions
|
|
105
|
+
|
|
106
|
+
| Convention | Route Example | URL | Handler Parameter |
|
|
107
|
+
|------------|---------------|-----|-------------------|
|
|
108
|
+
| `users/` | `app/users/route.py` | `/users` | — |
|
|
109
|
+
| `[id]/` | `app/users/[id]/route.py` | `/users/123` | `id: str` |
|
|
110
|
+
| `[[version]]/` | `app/api/[[version]]/route.py` | `/api` and `/api/v2` | `version: str \| None` |
|
|
111
|
+
| `[...path]/` | `app/files/[...path]/route.py` | `/files/a/b/c` | `path: str` |
|
|
112
|
+
| `(group)/` | `app/(admin)/settings/route.py` | `/settings` | — |
|
|
113
|
+
|
|
114
|
+
**Files:** `route.py` contains [handlers](#-route-handlers). `_middleware.py` contains [directory middleware](#directory-level-middleware) that cascades to all subdirectories.
|
|
115
|
+
|
|
116
|
+
## 📍 Route Handlers
|
|
117
|
+
|
|
118
|
+
Each `route.py` exports handlers. Supported HTTP methods: `get`, `post`, `put`, `patch`, `delete`, `head`, `options`, `websocket`. Functions prefixed with `_` are private helpers and ignored. Default status codes: `POST` → 201, `DELETE` → 204, all others → 200.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
# app/api/users/route.py
|
|
122
|
+
from fastapi_filebased_routing import route
|
|
123
|
+
|
|
124
|
+
# Module-level metadata (applies to all handlers in this file)
|
|
125
|
+
TAGS = ["users"] # auto-derived from first path segment if omitted
|
|
126
|
+
SUMMARY = "User management endpoints" # OpenAPI summary
|
|
127
|
+
DEPRECATED = True # mark all handlers as deprecated
|
|
128
|
+
|
|
129
|
+
# File-level middleware (applies to all handlers in this file, does NOT cascade to subdirectories)
|
|
130
|
+
middleware = [rate_limit(100)]
|
|
131
|
+
|
|
132
|
+
# Simple handler — just a function
|
|
133
|
+
async def get():
|
|
134
|
+
"""List users."""
|
|
135
|
+
return {"users": []}
|
|
136
|
+
|
|
137
|
+
# Configured handler — per-handler control over metadata and middleware
|
|
138
|
+
class post(route):
|
|
139
|
+
status_code = 200 # override convention-based 201
|
|
140
|
+
tags = ["admin"] # override module-level TAGS
|
|
141
|
+
summary = "Create a user" # override module-level SUMMARY
|
|
142
|
+
deprecated = True # override module-level DEPRECATED
|
|
143
|
+
middleware = [require_role("admin")] # or use inline: async def middleware(request, call_next): ...
|
|
144
|
+
|
|
145
|
+
async def handler(name: str):
|
|
146
|
+
return {"name": name}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Both styles coexist freely. Directory bracket names (e.g., `[user_id]`) become path parameters automatically injected into handler signatures. See [`examples/`](examples/) for complete working projects.
|
|
150
|
+
|
|
151
|
+
## 🔗 Middleware
|
|
152
|
+
|
|
153
|
+
Three-layer middleware system that lets you scope cross-cutting concerns (auth, logging, rate limiting) to directories, files, or individual handlers. Middleware is validated at startup and assembled into chains with zero per-request overhead.
|
|
154
|
+
|
|
155
|
+
All middleware functions use the same signature:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
async def my_middleware(request, call_next):
|
|
159
|
+
# Before handler
|
|
160
|
+
response = await call_next(request)
|
|
161
|
+
# After handler
|
|
162
|
+
return response
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Middleware must be `async`. Sync middleware raises a validation error at startup.
|
|
166
|
+
|
|
167
|
+
### Directory-Level Middleware
|
|
168
|
+
|
|
169
|
+
Create a `_middleware.py` file in any directory. Its middleware applies to all routes in that directory and all subdirectories. Use **one** of two forms:
|
|
170
|
+
|
|
171
|
+
**List form** — multiple middleware functions:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# app/api/_middleware.py — applies to all routes under /api/**
|
|
175
|
+
from app.auth import auth_required
|
|
176
|
+
from app.logging import request_logger
|
|
177
|
+
|
|
178
|
+
middleware = [auth_required, request_logger]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Single function form** — one inline middleware:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# app/_middleware.py — root-level timing middleware
|
|
185
|
+
import time
|
|
186
|
+
|
|
187
|
+
async def middleware(request, call_next):
|
|
188
|
+
start = time.monotonic()
|
|
189
|
+
response = await call_next(request)
|
|
190
|
+
response.headers["X-Response-Time"] = f"{time.monotonic() - start:.4f}"
|
|
191
|
+
return response
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Pick one form per file. If both are defined, standard Python name resolution applies — the last assignment to `middleware` wins.
|
|
195
|
+
|
|
196
|
+
Directory middleware cascades: a `_middleware.py` in `app/` applies to every route, while one in `app/api/` only applies to routes under `/api/`. Parent middleware always runs before child middleware.
|
|
197
|
+
|
|
198
|
+
### File-Level Middleware
|
|
199
|
+
|
|
200
|
+
Set `middleware = [...]` at the top of any `route.py` to apply middleware to all handlers in that file. Unlike directory middleware, file-level middleware does **not** cascade to subdirectories.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# app/api/users/route.py
|
|
204
|
+
middleware = [rate_limit(100)] # applies to get and post below, not to /api/users/[user_id]
|
|
205
|
+
|
|
206
|
+
async def get():
|
|
207
|
+
"""List users. Rate limited."""
|
|
208
|
+
return {"users": []}
|
|
209
|
+
|
|
210
|
+
async def post(name: str):
|
|
211
|
+
"""Create user. Rate limited."""
|
|
212
|
+
return {"name": name}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Handler-Level Middleware
|
|
216
|
+
|
|
217
|
+
`class handler(route):` blocks support a `middleware` attribute — as a list or a single inline function:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
# app/api/orders/route.py
|
|
221
|
+
from fastapi_filebased_routing import route
|
|
222
|
+
|
|
223
|
+
class post(route):
|
|
224
|
+
async def middleware(request, call_next):
|
|
225
|
+
if not request.headers.get("X-Idempotency-Key"):
|
|
226
|
+
from fastapi.responses import JSONResponse
|
|
227
|
+
return JSONResponse(
|
|
228
|
+
{"error": "X-Idempotency-Key header required"},
|
|
229
|
+
status_code=400,
|
|
230
|
+
)
|
|
231
|
+
return await call_next(request)
|
|
232
|
+
|
|
233
|
+
async def handler(order: dict):
|
|
234
|
+
"""Create order. Requires idempotency key."""
|
|
235
|
+
return {"order_id": "abc-123", **order}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Execution Order
|
|
239
|
+
|
|
240
|
+
When a request hits a route with middleware at multiple levels, they execute in this order:
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
Directory middleware (root → leaf)
|
|
244
|
+
→ File-level middleware
|
|
245
|
+
→ Handler-level middleware
|
|
246
|
+
→ Handler function
|
|
247
|
+
← Handler-level middleware
|
|
248
|
+
← File-level middleware
|
|
249
|
+
← Directory middleware
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Each middleware can modify the request before calling `call_next`, and modify the response after. Middleware can also short-circuit by returning a response without calling `call_next`:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
async def auth_guard(request, call_next):
|
|
256
|
+
if not request.headers.get("Authorization"):
|
|
257
|
+
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
|
258
|
+
return await call_next(request)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## 📦 Examples
|
|
262
|
+
|
|
263
|
+
See the [`examples/`](examples/) directory for runnable projects:
|
|
264
|
+
|
|
265
|
+
- **[`basic/`](examples/basic/)** — Routing fundamentals: static, dynamic, CRUD
|
|
266
|
+
- **[`middleware/`](examples/middleware/)** — All three middleware layers in action
|
|
267
|
+
- **[`advanced/`](examples/advanced/)** — Optional params, catch-all, route groups, WebSockets
|
|
268
|
+
|
|
269
|
+
## 📖 API Reference
|
|
270
|
+
|
|
271
|
+
### `create_router_from_path`
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
def create_router_from_path(
|
|
275
|
+
base_path: str | Path,
|
|
276
|
+
*,
|
|
277
|
+
prefix: str = "",
|
|
278
|
+
) -> APIRouter
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Create a FastAPI APIRouter from a directory of `route.py` files.
|
|
282
|
+
|
|
283
|
+
**Parameters:**
|
|
284
|
+
- `base_path` (str | Path): Root directory containing route.py files
|
|
285
|
+
- `prefix` (str, optional): URL prefix for all discovered routes (default: "")
|
|
286
|
+
|
|
287
|
+
**Returns:**
|
|
288
|
+
- `APIRouter`: A FastAPI router with all discovered routes registered
|
|
289
|
+
|
|
290
|
+
**Raises:**
|
|
291
|
+
- `RouteDiscoveryError`: If base_path doesn't exist or isn't a directory
|
|
292
|
+
- `RouteValidationError`: If a route file has invalid exports or parameters
|
|
293
|
+
- `DuplicateRouteError`: If two route files resolve to the same path+method
|
|
294
|
+
- `PathParseError`: If a directory name has invalid syntax
|
|
295
|
+
- `MiddlewareValidationError`: If a `_middleware.py` file fails to import or contains invalid middleware
|
|
296
|
+
|
|
297
|
+
**Example:**
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
from fastapi import FastAPI
|
|
301
|
+
from fastapi_filebased_routing import create_router_from_path
|
|
302
|
+
|
|
303
|
+
app = FastAPI()
|
|
304
|
+
|
|
305
|
+
# Basic usage
|
|
306
|
+
app.include_router(create_router_from_path("app"))
|
|
307
|
+
|
|
308
|
+
# With prefix
|
|
309
|
+
app.include_router(create_router_from_path("app", prefix="/api/v1"))
|
|
310
|
+
|
|
311
|
+
# Multiple routers
|
|
312
|
+
app.include_router(create_router_from_path("app/public"))
|
|
313
|
+
app.include_router(create_router_from_path("app/admin", prefix="/admin"))
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### `route`
|
|
317
|
+
|
|
318
|
+
Base class for handler-level middleware and metadata configuration. Uses a metaclass that returns a `RouteConfig` instead of a class.
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
from fastapi_filebased_routing import route
|
|
322
|
+
|
|
323
|
+
class get(route):
|
|
324
|
+
middleware = [auth_required]
|
|
325
|
+
tags = ["users"]
|
|
326
|
+
summary = "Get user details"
|
|
327
|
+
|
|
328
|
+
async def handler(user_id: str):
|
|
329
|
+
return {"user_id": user_id}
|
|
330
|
+
|
|
331
|
+
# `get` is now a RouteConfig, not a class
|
|
332
|
+
# `get(user_id="123")` calls the handler directly
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## 💡 Why This Plugin?
|
|
336
|
+
|
|
337
|
+
FastAPI developers building medium-to-large APIs face these problems:
|
|
338
|
+
|
|
339
|
+
1. **Manual route registration is tedious.** Every endpoint requires updating a centralized router file.
|
|
340
|
+
2. **Route discoverability degrades.** Finding the handler for `/api/v1/users/{id}` requires searching across files.
|
|
341
|
+
3. **Middleware wiring is repetitive.** Applying auth to 20 admin endpoints means 20 copies of `Depends(require_admin)`.
|
|
342
|
+
4. **Full-stack developers experience friction.** Next.js has file-based routing, FastAPI requires manual wiring.
|
|
343
|
+
|
|
344
|
+
This plugin solves all four with:
|
|
345
|
+
|
|
346
|
+
- Zero-configuration route discovery from directory structure
|
|
347
|
+
- Three-layer middleware system (directory, file, handler) with cascading inheritance
|
|
348
|
+
- Next.js feature parity: dynamic params, optional params, catch-all, route groups
|
|
349
|
+
- Convention over configuration for status codes, tags, and metadata
|
|
350
|
+
- Battle-tested security (path traversal protection, symlink validation)
|
|
351
|
+
- WebSocket support, sync and async handlers
|
|
352
|
+
- Hot reload compatible (`uvicorn --reload` works out of the box)
|
|
353
|
+
- Full mypy strict mode support, coexists with manual FastAPI routing
|
|
354
|
+
|
|
355
|
+
## 🤝 Contributing
|
|
356
|
+
|
|
357
|
+
This plugin is extracted from a production codebase and is actively maintained. Issues, feature requests, and pull requests are welcome.
|
|
358
|
+
|
|
359
|
+
GitHub: https://github.com/rsmdt/fastapi-filebased-routing.py
|