jec-api 0.0.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.
- jec_api-0.0.1/.gitattributes +2 -0
- jec_api-0.0.1/.gitignore +29 -0
- jec_api-0.0.1/LICENSE +21 -0
- jec_api-0.0.1/PKG-INFO +134 -0
- jec_api-0.0.1/README.md +106 -0
- jec_api-0.0.1/pyproject.toml +44 -0
- jec_api-0.0.1/src/jec_api/__init__.py +7 -0
- jec_api-0.0.1/src/jec_api/discovery.py +145 -0
- jec_api-0.0.1/src/jec_api/route.py +118 -0
- jec_api-0.0.1/src/jec_api/router.py +105 -0
jec_api-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Byte-compiled
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
*.egg
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
venv/
|
|
14
|
+
.venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.idea/
|
|
19
|
+
.vscode/
|
|
20
|
+
*.swp
|
|
21
|
+
|
|
22
|
+
# Testing
|
|
23
|
+
.pytest_cache/
|
|
24
|
+
.coverage
|
|
25
|
+
htmlcov/
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
jec_api-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nik
|
|
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.
|
jec_api-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jec-api
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Beta version of JEC API
|
|
5
|
+
Project-URL: Homepage, https://github.com/alpheay/jec
|
|
6
|
+
Project-URL: Repository, https://github.com/alpheay/jec
|
|
7
|
+
Author: Nik
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: api,class-based,fastapi,routes,web
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: fastapi>=0.100.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: httpx>=0.24.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: uvicorn>=0.20.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# JEC-API
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **Class-Based Routes**: Group related endpoints (e.g., CRUD operations) into a single class.
|
|
34
|
+
- **Auto-Discovery**: Automatically find and register route classes from your project packages.
|
|
35
|
+
- **Smart Method Naming**: API paths and HTTP methods are inferred from your method names (e.g., `get_by_id` becomes `GET /{id}`).
|
|
36
|
+
- **FastAPI Native**: Fully compatible with FastAPI dependencies, models, and OpenAPI generation.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install jec-api
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
1. **Define a Route Class**
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# routes.py
|
|
50
|
+
from jec_api import Route
|
|
51
|
+
|
|
52
|
+
class Users(Route):
|
|
53
|
+
# Optional: explicitly set path, otherwise defaults to /users
|
|
54
|
+
# path = "/my-users"
|
|
55
|
+
|
|
56
|
+
async def get(self):
|
|
57
|
+
"""List all users"""
|
|
58
|
+
return [{"id": 1, "name": "Alice"}]
|
|
59
|
+
|
|
60
|
+
async def get_by_id(self, id: int):
|
|
61
|
+
"""Get user by ID"""
|
|
62
|
+
return {"id": id, "name": "Alice"}
|
|
63
|
+
|
|
64
|
+
async def post(self, name: str):
|
|
65
|
+
"""Create a user"""
|
|
66
|
+
return {"id": 2, "name": name}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
2. **Create the App**
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# main.py
|
|
73
|
+
from jec_api import Core
|
|
74
|
+
|
|
75
|
+
core = Core(title="My API")
|
|
76
|
+
|
|
77
|
+
# Auto-discover routes from a module/package
|
|
78
|
+
core.discover("routes")
|
|
79
|
+
|
|
80
|
+
# Or register manually
|
|
81
|
+
from routes import Users
|
|
82
|
+
core.register(Users)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
3. **Run it**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uvicorn main:core --reload
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage Guide
|
|
92
|
+
|
|
93
|
+
### Defining Routes
|
|
94
|
+
|
|
95
|
+
Inherit from `jec_api.Route` to create a route group. The class name is automatically converted to kebab-case to form the base path (e.g., `UserProfiles` -> `/user-profiles`), unless you override it with the `path` attribute.
|
|
96
|
+
|
|
97
|
+
### Method Naming Convention
|
|
98
|
+
|
|
99
|
+
JEC-API parses your method names to determine the HTTP verb and path parameters:
|
|
100
|
+
|
|
101
|
+
| Method Name | HTTP Verb | Generated Path |
|
|
102
|
+
|-------------|-----------|----------------|
|
|
103
|
+
| `get()` | GET | `/` |
|
|
104
|
+
| `post()` | POST | `/` |
|
|
105
|
+
| `get_by_id(id)` | GET | `/{id}` |
|
|
106
|
+
| `delete_by_id(id)` | DELETE | `/{id}` |
|
|
107
|
+
| `get_users()` | GET | `/users` |
|
|
108
|
+
| `post_batch_update()` | POST | `/batch-update` |
|
|
109
|
+
|
|
110
|
+
### Path Parameters
|
|
111
|
+
|
|
112
|
+
To define path parameters, use the `_by_{param}` pattern in your method name.
|
|
113
|
+
For example, `get_by_user_id` will generate a path `/{user_id}`.
|
|
114
|
+
|
|
115
|
+
### Manual Registration
|
|
116
|
+
|
|
117
|
+
You can register routes manually if you prefer not to use auto-discovery:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from my_routes import MyRoute
|
|
121
|
+
app.register(MyRoute, tags=["Custom Tag"])
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Auto-Discovery
|
|
125
|
+
|
|
126
|
+
The `discover()` method recursively searches the specified package for any classes inheriting from `Route` and registers them.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
app.discover("src.routes")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT License
|
jec_api-0.0.1/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# JEC-API
|
|
2
|
+
|
|
3
|
+
## Features
|
|
4
|
+
|
|
5
|
+
- **Class-Based Routes**: Group related endpoints (e.g., CRUD operations) into a single class.
|
|
6
|
+
- **Auto-Discovery**: Automatically find and register route classes from your project packages.
|
|
7
|
+
- **Smart Method Naming**: API paths and HTTP methods are inferred from your method names (e.g., `get_by_id` becomes `GET /{id}`).
|
|
8
|
+
- **FastAPI Native**: Fully compatible with FastAPI dependencies, models, and OpenAPI generation.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install jec-api
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
1. **Define a Route Class**
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
# routes.py
|
|
22
|
+
from jec_api import Route
|
|
23
|
+
|
|
24
|
+
class Users(Route):
|
|
25
|
+
# Optional: explicitly set path, otherwise defaults to /users
|
|
26
|
+
# path = "/my-users"
|
|
27
|
+
|
|
28
|
+
async def get(self):
|
|
29
|
+
"""List all users"""
|
|
30
|
+
return [{"id": 1, "name": "Alice"}]
|
|
31
|
+
|
|
32
|
+
async def get_by_id(self, id: int):
|
|
33
|
+
"""Get user by ID"""
|
|
34
|
+
return {"id": id, "name": "Alice"}
|
|
35
|
+
|
|
36
|
+
async def post(self, name: str):
|
|
37
|
+
"""Create a user"""
|
|
38
|
+
return {"id": 2, "name": name}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
2. **Create the App**
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# main.py
|
|
45
|
+
from jec_api import Core
|
|
46
|
+
|
|
47
|
+
core = Core(title="My API")
|
|
48
|
+
|
|
49
|
+
# Auto-discover routes from a module/package
|
|
50
|
+
core.discover("routes")
|
|
51
|
+
|
|
52
|
+
# Or register manually
|
|
53
|
+
from routes import Users
|
|
54
|
+
core.register(Users)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
3. **Run it**
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uvicorn main:core --reload
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage Guide
|
|
64
|
+
|
|
65
|
+
### Defining Routes
|
|
66
|
+
|
|
67
|
+
Inherit from `jec_api.Route` to create a route group. The class name is automatically converted to kebab-case to form the base path (e.g., `UserProfiles` -> `/user-profiles`), unless you override it with the `path` attribute.
|
|
68
|
+
|
|
69
|
+
### Method Naming Convention
|
|
70
|
+
|
|
71
|
+
JEC-API parses your method names to determine the HTTP verb and path parameters:
|
|
72
|
+
|
|
73
|
+
| Method Name | HTTP Verb | Generated Path |
|
|
74
|
+
|-------------|-----------|----------------|
|
|
75
|
+
| `get()` | GET | `/` |
|
|
76
|
+
| `post()` | POST | `/` |
|
|
77
|
+
| `get_by_id(id)` | GET | `/{id}` |
|
|
78
|
+
| `delete_by_id(id)` | DELETE | `/{id}` |
|
|
79
|
+
| `get_users()` | GET | `/users` |
|
|
80
|
+
| `post_batch_update()` | POST | `/batch-update` |
|
|
81
|
+
|
|
82
|
+
### Path Parameters
|
|
83
|
+
|
|
84
|
+
To define path parameters, use the `_by_{param}` pattern in your method name.
|
|
85
|
+
For example, `get_by_user_id` will generate a path `/{user_id}`.
|
|
86
|
+
|
|
87
|
+
### Manual Registration
|
|
88
|
+
|
|
89
|
+
You can register routes manually if you prefer not to use auto-discovery:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from my_routes import MyRoute
|
|
93
|
+
app.register(MyRoute, tags=["Custom Tag"])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Auto-Discovery
|
|
97
|
+
|
|
98
|
+
The `discover()` method recursively searches the specified package for any classes inheriting from `Route` and registers them.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
app.discover("src.routes")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT License
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jec-api"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Beta version of JEC API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Nik" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["fastapi", "api", "routes", "class-based", "web"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Framework :: FastAPI",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"fastapi>=0.100.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=7.0.0",
|
|
35
|
+
"httpx>=0.24.0",
|
|
36
|
+
"uvicorn>=0.20.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/alpheay/jec"
|
|
41
|
+
Repository = "https://github.com/alpheay/jec"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/jec_api"]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Auto-discovery of Route classes from packages and directories."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import importlib.util
|
|
5
|
+
import pkgutil
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Type
|
|
9
|
+
|
|
10
|
+
from .route import Route
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def discover_routes(package: str, *, recursive: bool = True) -> List[Type[Route]]:
|
|
14
|
+
"""
|
|
15
|
+
Discover all Route subclasses in a package or directory.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
package: Package name (e.g., "routes") or path to directory
|
|
19
|
+
recursive: Whether to search subdirectories
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of Route subclasses found
|
|
23
|
+
"""
|
|
24
|
+
route_classes: List[Type[Route]] = []
|
|
25
|
+
|
|
26
|
+
# Check if it's a path or a package name
|
|
27
|
+
package_path = Path(package)
|
|
28
|
+
|
|
29
|
+
if package_path.is_dir():
|
|
30
|
+
# It's a directory path
|
|
31
|
+
route_classes.extend(_discover_from_directory(package_path, recursive))
|
|
32
|
+
else:
|
|
33
|
+
# Try as a package name
|
|
34
|
+
try:
|
|
35
|
+
route_classes.extend(_discover_from_package(package, recursive))
|
|
36
|
+
except ModuleNotFoundError:
|
|
37
|
+
# Maybe it's a relative path from cwd
|
|
38
|
+
cwd_path = Path.cwd() / package
|
|
39
|
+
if cwd_path.is_dir():
|
|
40
|
+
route_classes.extend(_discover_from_directory(cwd_path, recursive))
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(f"Could not find package or directory: {package}")
|
|
43
|
+
|
|
44
|
+
return route_classes
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _discover_from_package(package_name: str, recursive: bool) -> List[Type[Route]]:
|
|
48
|
+
"""Discover routes from an installed package."""
|
|
49
|
+
route_classes: List[Type[Route]] = []
|
|
50
|
+
|
|
51
|
+
package = importlib.import_module(package_name)
|
|
52
|
+
|
|
53
|
+
if not hasattr(package, "__path__"):
|
|
54
|
+
# Single module, not a package
|
|
55
|
+
route_classes.extend(_extract_routes_from_module(package))
|
|
56
|
+
return route_classes
|
|
57
|
+
|
|
58
|
+
# Walk through the package
|
|
59
|
+
prefix = package_name + "."
|
|
60
|
+
|
|
61
|
+
for importer, modname, ispkg in pkgutil.walk_packages(
|
|
62
|
+
package.__path__,
|
|
63
|
+
prefix=prefix,
|
|
64
|
+
):
|
|
65
|
+
if not recursive and ispkg:
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
module = importlib.import_module(modname)
|
|
70
|
+
route_classes.extend(_extract_routes_from_module(module))
|
|
71
|
+
except Exception:
|
|
72
|
+
# Skip modules that fail to import
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
return route_classes
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _discover_from_directory(directory: Path, recursive: bool) -> List[Type[Route]]:
|
|
79
|
+
"""Discover routes from a directory of Python files."""
|
|
80
|
+
route_classes: List[Type[Route]] = []
|
|
81
|
+
|
|
82
|
+
# Add directory to sys.path temporarily if needed
|
|
83
|
+
dir_str = str(directory.parent.resolve())
|
|
84
|
+
added_to_path = False
|
|
85
|
+
|
|
86
|
+
if dir_str not in sys.path:
|
|
87
|
+
sys.path.insert(0, dir_str)
|
|
88
|
+
added_to_path = True
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
pattern = "**/*.py" if recursive else "*.py"
|
|
92
|
+
|
|
93
|
+
for py_file in directory.glob(pattern):
|
|
94
|
+
if py_file.name.startswith("_"):
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
module = _load_module_from_file(py_file)
|
|
99
|
+
if module:
|
|
100
|
+
route_classes.extend(_extract_routes_from_module(module))
|
|
101
|
+
except Exception:
|
|
102
|
+
# Skip files that fail to import
|
|
103
|
+
continue
|
|
104
|
+
finally:
|
|
105
|
+
if added_to_path:
|
|
106
|
+
sys.path.remove(dir_str)
|
|
107
|
+
|
|
108
|
+
return route_classes
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _load_module_from_file(file_path: Path):
|
|
112
|
+
"""Load a Python module from a file path."""
|
|
113
|
+
module_name = file_path.stem
|
|
114
|
+
|
|
115
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
116
|
+
if spec is None or spec.loader is None:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
module = importlib.util.module_from_spec(spec)
|
|
120
|
+
sys.modules[module_name] = module
|
|
121
|
+
spec.loader.exec_module(module)
|
|
122
|
+
|
|
123
|
+
return module
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _extract_routes_from_module(module) -> List[Type[Route]]:
|
|
127
|
+
"""Extract all Route subclasses from a module."""
|
|
128
|
+
route_classes: List[Type[Route]] = []
|
|
129
|
+
|
|
130
|
+
for name in dir(module):
|
|
131
|
+
if name.startswith("_"):
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
obj = getattr(module, name)
|
|
135
|
+
|
|
136
|
+
# Check if it's a class that inherits from Route (but not Route itself)
|
|
137
|
+
if (
|
|
138
|
+
isinstance(obj, type)
|
|
139
|
+
and issubclass(obj, Route)
|
|
140
|
+
and obj is not Route
|
|
141
|
+
and obj.__module__ == module.__name__
|
|
142
|
+
):
|
|
143
|
+
route_classes.append(obj)
|
|
144
|
+
|
|
145
|
+
return route_classes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Route base class for defining API endpoints."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Type
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# HTTP methods that can be used as method prefixes
|
|
9
|
+
HTTP_METHODS = {"get", "post", "put", "delete", "patch", "options", "head"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RouteMeta(type):
|
|
13
|
+
"""Metaclass that collects route information from class methods."""
|
|
14
|
+
|
|
15
|
+
def __new__(mcs, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> type:
|
|
16
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
17
|
+
|
|
18
|
+
# Skip processing for the base Route class itself
|
|
19
|
+
if name == "Route" and not bases:
|
|
20
|
+
return cls
|
|
21
|
+
|
|
22
|
+
# Collect endpoint methods
|
|
23
|
+
cls._endpoints: List[Tuple[str, str, Callable]] = []
|
|
24
|
+
|
|
25
|
+
for attr_name, attr_value in namespace.items():
|
|
26
|
+
if attr_name.startswith("_"):
|
|
27
|
+
continue
|
|
28
|
+
if not callable(attr_value):
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
parsed = mcs._parse_method_name(attr_name)
|
|
32
|
+
if parsed:
|
|
33
|
+
http_method, sub_path = parsed
|
|
34
|
+
cls._endpoints.append((http_method, sub_path, attr_value))
|
|
35
|
+
|
|
36
|
+
return cls
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _parse_method_name(name: str) -> Optional[Tuple[str, str]]:
|
|
40
|
+
"""
|
|
41
|
+
Parse method name to extract HTTP method and sub-path.
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
get -> (GET, /)
|
|
45
|
+
post -> (POST, /)
|
|
46
|
+
get_by_id -> (GET, /{id})
|
|
47
|
+
get_users -> (GET, /users)
|
|
48
|
+
post_batch -> (POST, /batch)
|
|
49
|
+
get_user_by_id -> (GET, /user/{id})
|
|
50
|
+
"""
|
|
51
|
+
parts = name.lower().split("_")
|
|
52
|
+
|
|
53
|
+
if not parts or parts[0] not in HTTP_METHODS:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
http_method = parts[0].upper()
|
|
57
|
+
|
|
58
|
+
if len(parts) == 1:
|
|
59
|
+
# Just the HTTP method: get, post, etc.
|
|
60
|
+
return (http_method, "/")
|
|
61
|
+
|
|
62
|
+
# Check for "by_" pattern indicating path parameter
|
|
63
|
+
path_parts = []
|
|
64
|
+
i = 1
|
|
65
|
+
while i < len(parts):
|
|
66
|
+
if parts[i] == "by" and i + 1 < len(parts):
|
|
67
|
+
# Convert "by_id" to "{id}"
|
|
68
|
+
param_name = parts[i + 1]
|
|
69
|
+
path_parts.append(f"{{{param_name}}}")
|
|
70
|
+
i += 2
|
|
71
|
+
else:
|
|
72
|
+
# Convert to kebab-case path segment
|
|
73
|
+
path_parts.append(parts[i])
|
|
74
|
+
i += 1
|
|
75
|
+
|
|
76
|
+
if not path_parts:
|
|
77
|
+
return (http_method, "/")
|
|
78
|
+
|
|
79
|
+
sub_path = "/" + "/".join(path_parts)
|
|
80
|
+
return (http_method, sub_path)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Route(metaclass=RouteMeta):
|
|
84
|
+
"""
|
|
85
|
+
Base class for defining API route endpoints.
|
|
86
|
+
|
|
87
|
+
Inherit from this class and define methods with HTTP method prefixes:
|
|
88
|
+
- get(), post(), put(), delete(), patch(), options(), head()
|
|
89
|
+
- get_by_id(id: int) -> GET /{id}
|
|
90
|
+
- get_users() -> GET /users
|
|
91
|
+
- post_batch() -> POST /batch
|
|
92
|
+
|
|
93
|
+
Optionally set `path` class attribute to override the auto-generated path.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# Override this to set a custom path instead of deriving from class name
|
|
97
|
+
path: Optional[str] = None
|
|
98
|
+
|
|
99
|
+
# Set by metaclass
|
|
100
|
+
_endpoints: List[Tuple[str, str, Callable]] = []
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def get_path(cls) -> str:
|
|
104
|
+
"""Get the base path for this route class."""
|
|
105
|
+
if cls.path is not None:
|
|
106
|
+
return cls.path if cls.path.startswith("/") else f"/{cls.path}"
|
|
107
|
+
|
|
108
|
+
# Convert class name to kebab-case path
|
|
109
|
+
# UserProfiles -> user-profiles
|
|
110
|
+
name = cls.__name__
|
|
111
|
+
# Insert hyphens before uppercase letters and lowercase everything
|
|
112
|
+
kebab = re.sub(r"(?<!^)(?=[A-Z])", "-", name).lower()
|
|
113
|
+
return f"/{kebab}"
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def get_endpoints(cls) -> List[Tuple[str, str, Callable]]:
|
|
117
|
+
"""Get all endpoint definitions for this route class."""
|
|
118
|
+
return cls._endpoints
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Core - FastAPI wrapper with class-based route registration."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, List, Type, Optional
|
|
4
|
+
from fastapi import FastAPI, APIRouter
|
|
5
|
+
from fastapi.routing import APIRoute
|
|
6
|
+
|
|
7
|
+
from .route import Route
|
|
8
|
+
from .discovery import discover_routes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Core(FastAPI):
|
|
12
|
+
"""
|
|
13
|
+
FastAPI application with class-based route registration.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
app = Core()
|
|
17
|
+
app.discover("routes") # Auto-discover from package
|
|
18
|
+
app.register(MyRoute) # Or register manually
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, *args, **kwargs):
|
|
22
|
+
super().__init__(*args, **kwargs)
|
|
23
|
+
self._registered_routes: List[Type[Route]] = []
|
|
24
|
+
|
|
25
|
+
def register(self, route_class: Type[Route], **router_kwargs) -> "Core":
|
|
26
|
+
"""
|
|
27
|
+
Register a Route subclass with the application.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
route_class: A class that inherits from Route
|
|
31
|
+
**router_kwargs: Additional kwargs passed to APIRouter (tags, etc.)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Self for method chaining
|
|
35
|
+
"""
|
|
36
|
+
if not isinstance(route_class, type) or not issubclass(route_class, Route):
|
|
37
|
+
raise TypeError(f"{route_class} must be a subclass of Route")
|
|
38
|
+
|
|
39
|
+
if route_class is Route:
|
|
40
|
+
raise ValueError("Cannot register the base Route class directly")
|
|
41
|
+
|
|
42
|
+
base_path = route_class.get_path()
|
|
43
|
+
endpoints = route_class.get_endpoints()
|
|
44
|
+
|
|
45
|
+
if not endpoints:
|
|
46
|
+
return self # No endpoints to register
|
|
47
|
+
|
|
48
|
+
# Create an instance of the route class
|
|
49
|
+
instance = route_class()
|
|
50
|
+
|
|
51
|
+
# Determine tags from class name if not provided
|
|
52
|
+
if "tags" not in router_kwargs:
|
|
53
|
+
router_kwargs["tags"] = [route_class.__name__]
|
|
54
|
+
|
|
55
|
+
# Register each endpoint
|
|
56
|
+
for http_method, sub_path, method_func in endpoints:
|
|
57
|
+
# Build full path
|
|
58
|
+
if sub_path == "/":
|
|
59
|
+
full_path = base_path
|
|
60
|
+
else:
|
|
61
|
+
full_path = base_path.rstrip("/") + sub_path
|
|
62
|
+
|
|
63
|
+
# Bind the method to the instance
|
|
64
|
+
bound_method = getattr(instance, method_func.__name__)
|
|
65
|
+
|
|
66
|
+
# Get the appropriate router method (get, post, etc.)
|
|
67
|
+
router_method = getattr(self, http_method.lower())
|
|
68
|
+
|
|
69
|
+
# Register the route
|
|
70
|
+
router_method(
|
|
71
|
+
full_path,
|
|
72
|
+
**router_kwargs,
|
|
73
|
+
)(bound_method)
|
|
74
|
+
|
|
75
|
+
self._registered_routes.append(route_class)
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def discover(
|
|
79
|
+
self,
|
|
80
|
+
package: str,
|
|
81
|
+
*,
|
|
82
|
+
recursive: bool = True,
|
|
83
|
+
**router_kwargs
|
|
84
|
+
) -> "Core":
|
|
85
|
+
"""
|
|
86
|
+
Auto-discover and register Route subclasses from a package.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
package: Package name or path to discover routes from
|
|
90
|
+
recursive: Whether to search subdirectories
|
|
91
|
+
**router_kwargs: Additional kwargs passed to each route's registration
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Self for method chaining
|
|
95
|
+
"""
|
|
96
|
+
route_classes = discover_routes(package, recursive=recursive)
|
|
97
|
+
|
|
98
|
+
for route_class in route_classes:
|
|
99
|
+
self.register(route_class, **router_kwargs)
|
|
100
|
+
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def get_registered_routes(self) -> List[Type[Route]]:
|
|
104
|
+
"""Get a list of all registered Route classes."""
|
|
105
|
+
return self._registered_routes.copy()
|