classapi 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.
- classapi-0.1.0/PKG-INFO +110 -0
- classapi-0.1.0/README.md +102 -0
- classapi-0.1.0/classapi/__init__.py +2 -0
- classapi-0.1.0/classapi/classapi.py +270 -0
- classapi-0.1.0/classapi.egg-info/PKG-INFO +110 -0
- classapi-0.1.0/classapi.egg-info/SOURCES.txt +9 -0
- classapi-0.1.0/classapi.egg-info/dependency_links.txt +1 -0
- classapi-0.1.0/classapi.egg-info/requires.txt +1 -0
- classapi-0.1.0/classapi.egg-info/top_level.txt +1 -0
- classapi-0.1.0/pyproject.toml +9 -0
- classapi-0.1.0/setup.cfg +4 -0
classapi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: classapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastapi[standard]>=0.129.0
|
|
8
|
+
|
|
9
|
+
# ClassApi
|
|
10
|
+
ClassApi is a small convenience layer on top of FastAPI that enables class-based views (CBV). It preserves FastAPI's features (typing, dependencies, automatic docs) while letting you organize handlers as classes.
|
|
11
|
+
|
|
12
|
+
**Key features**
|
|
13
|
+
- Use `BaseView` as a base class for handlers (`get`, `post`, `put`, ...).
|
|
14
|
+
- Support for `pre_process` and `pre_<method>` hooks to validate or transform requests.
|
|
15
|
+
- Register route modules by module path (supports relative module paths like `.src.urls`).
|
|
16
|
+
- Combined signatures from `pre_process` and the handler method are exposed to FastAPI for documentation and form generation.
|
|
17
|
+
|
|
18
|
+
**Development setup (using `uv` helper)**
|
|
19
|
+
1. Create a virtual environment:
|
|
20
|
+
```bash
|
|
21
|
+
python -m venv .venv
|
|
22
|
+
```
|
|
23
|
+
2. Use your `uv` helper to run `pip` inside the project environment and install dependencies:
|
|
24
|
+
```bash
|
|
25
|
+
uv run pip install fastapi uvicorn
|
|
26
|
+
# or install from requirements: uv run pip install -r requirements.txt
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Quickstart**
|
|
30
|
+
Create `main.py`:
|
|
31
|
+
```py
|
|
32
|
+
from classapi import ClassApi
|
|
33
|
+
|
|
34
|
+
app = ClassApi()
|
|
35
|
+
|
|
36
|
+
app.include_routers(".src.urls")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Example routes and views layout (tests/app_test/src):
|
|
40
|
+
```py
|
|
41
|
+
# tests/app_test/src/urls.py
|
|
42
|
+
from .views import HelloWorldView
|
|
43
|
+
|
|
44
|
+
urls = [
|
|
45
|
+
{"path": "/hello", "view": HelloWorldView}
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# tests/app_test/src/views.py
|
|
49
|
+
from classapi import View, Header, HTTPException
|
|
50
|
+
from typing import Annotated
|
|
51
|
+
|
|
52
|
+
class ValidateUser(BaseView):
|
|
53
|
+
def pre_process(self, jwt: Annotated[str | None, Header()] = None):
|
|
54
|
+
if jwt != "valid_jwt":
|
|
55
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
56
|
+
|
|
57
|
+
class HelloWorldView(ValidateUser, BaseView):
|
|
58
|
+
methods = ["GET"]
|
|
59
|
+
|
|
60
|
+
def get(self, name: str = "World"):
|
|
61
|
+
return {"Hello": name}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Supported `urls` formats**
|
|
65
|
+
- Dict entries: `{"path": "/x", "view": MyView, ...fastapi kwargs...}` — extra kwargs (e.g. `response_model`) are forwarded to `add_api_route`.
|
|
66
|
+
- Tuple/list entries: `("/x", MyView)`.
|
|
67
|
+
- You may use relative imports from the calling module: `app.include_routers(".src.urls")`.
|
|
68
|
+
|
|
69
|
+
**`View` classes**
|
|
70
|
+
- Define HTTP methods: `get`, `post`, `put`, `delete`, `patch`.
|
|
71
|
+
- Limit exposed methods with `methods = ["GET"]` on the class.
|
|
72
|
+
- Hooks:
|
|
73
|
+
- `pre_process(self, ...)` — runs before any handler.
|
|
74
|
+
- `pre_get(self, ...)`, `pre_post(...)`, ... — run before a specific handler.
|
|
75
|
+
- Signatures from `pre_process`, `pre_<method>` and the handler itself are merged and exposed to FastAPI; annotate parameters with `Annotated[..., Header()]`, `Cookie()`, etc., to appear correctly in `/docs`.
|
|
76
|
+
|
|
77
|
+
Example: header extraction in `pre_process`:
|
|
78
|
+
```py
|
|
79
|
+
def pre_process(self, jwt: Annotated[str|None, Header()] = None):
|
|
80
|
+
...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If you accidentally place `Annotated[...]` as a default (e.g. `jwt = Annotated[...]`), ClassApi attempts to normalize it so FastAPI recognizes the dependency. Still, annotate parameters properly when possible.
|
|
84
|
+
|
|
85
|
+
**Running the app**
|
|
86
|
+
- Use `uv` to run the app with reload during development:
|
|
87
|
+
```bash
|
|
88
|
+
uv run uvicorn tests.app_test.main:app --reload
|
|
89
|
+
```
|
|
90
|
+
or, if you use the helper script `test_init.py` at the repository root:
|
|
91
|
+
```bash
|
|
92
|
+
uv run .\test_init.py
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Debugging endpoint signatures**
|
|
96
|
+
If docs don't show expected parameters, you can inspect endpoint signatures at runtime:
|
|
97
|
+
```py
|
|
98
|
+
for r in app.routes:
|
|
99
|
+
print(r.path, getattr(r.endpoint, '__signature__', None))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Editor integration (VSCode / Pylance)**
|
|
103
|
+
Pylance is a static analyzer and doesn't pick up runtime-generated signatures. To get editor hovers that match your runtime docs, create a `.pyi` stub next to your views module describing the public signatures (this does not change runtime behavior).
|
|
104
|
+
|
|
105
|
+
**Contributing**
|
|
106
|
+
- Open issues or PRs.
|
|
107
|
+
- Add tests under `tests/` and run them with `pytest`.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
If you want, I can generate a `views.pyi` stub for your views, add example tests, or add a minimal `pyproject.toml`. Which should I do next?
|
classapi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# ClassApi
|
|
2
|
+
ClassApi is a small convenience layer on top of FastAPI that enables class-based views (CBV). It preserves FastAPI's features (typing, dependencies, automatic docs) while letting you organize handlers as classes.
|
|
3
|
+
|
|
4
|
+
**Key features**
|
|
5
|
+
- Use `BaseView` as a base class for handlers (`get`, `post`, `put`, ...).
|
|
6
|
+
- Support for `pre_process` and `pre_<method>` hooks to validate or transform requests.
|
|
7
|
+
- Register route modules by module path (supports relative module paths like `.src.urls`).
|
|
8
|
+
- Combined signatures from `pre_process` and the handler method are exposed to FastAPI for documentation and form generation.
|
|
9
|
+
|
|
10
|
+
**Development setup (using `uv` helper)**
|
|
11
|
+
1. Create a virtual environment:
|
|
12
|
+
```bash
|
|
13
|
+
python -m venv .venv
|
|
14
|
+
```
|
|
15
|
+
2. Use your `uv` helper to run `pip` inside the project environment and install dependencies:
|
|
16
|
+
```bash
|
|
17
|
+
uv run pip install fastapi uvicorn
|
|
18
|
+
# or install from requirements: uv run pip install -r requirements.txt
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Quickstart**
|
|
22
|
+
Create `main.py`:
|
|
23
|
+
```py
|
|
24
|
+
from classapi import ClassApi
|
|
25
|
+
|
|
26
|
+
app = ClassApi()
|
|
27
|
+
|
|
28
|
+
app.include_routers(".src.urls")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Example routes and views layout (tests/app_test/src):
|
|
32
|
+
```py
|
|
33
|
+
# tests/app_test/src/urls.py
|
|
34
|
+
from .views import HelloWorldView
|
|
35
|
+
|
|
36
|
+
urls = [
|
|
37
|
+
{"path": "/hello", "view": HelloWorldView}
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# tests/app_test/src/views.py
|
|
41
|
+
from classapi import View, Header, HTTPException
|
|
42
|
+
from typing import Annotated
|
|
43
|
+
|
|
44
|
+
class ValidateUser(BaseView):
|
|
45
|
+
def pre_process(self, jwt: Annotated[str | None, Header()] = None):
|
|
46
|
+
if jwt != "valid_jwt":
|
|
47
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
48
|
+
|
|
49
|
+
class HelloWorldView(ValidateUser, BaseView):
|
|
50
|
+
methods = ["GET"]
|
|
51
|
+
|
|
52
|
+
def get(self, name: str = "World"):
|
|
53
|
+
return {"Hello": name}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Supported `urls` formats**
|
|
57
|
+
- Dict entries: `{"path": "/x", "view": MyView, ...fastapi kwargs...}` — extra kwargs (e.g. `response_model`) are forwarded to `add_api_route`.
|
|
58
|
+
- Tuple/list entries: `("/x", MyView)`.
|
|
59
|
+
- You may use relative imports from the calling module: `app.include_routers(".src.urls")`.
|
|
60
|
+
|
|
61
|
+
**`View` classes**
|
|
62
|
+
- Define HTTP methods: `get`, `post`, `put`, `delete`, `patch`.
|
|
63
|
+
- Limit exposed methods with `methods = ["GET"]` on the class.
|
|
64
|
+
- Hooks:
|
|
65
|
+
- `pre_process(self, ...)` — runs before any handler.
|
|
66
|
+
- `pre_get(self, ...)`, `pre_post(...)`, ... — run before a specific handler.
|
|
67
|
+
- Signatures from `pre_process`, `pre_<method>` and the handler itself are merged and exposed to FastAPI; annotate parameters with `Annotated[..., Header()]`, `Cookie()`, etc., to appear correctly in `/docs`.
|
|
68
|
+
|
|
69
|
+
Example: header extraction in `pre_process`:
|
|
70
|
+
```py
|
|
71
|
+
def pre_process(self, jwt: Annotated[str|None, Header()] = None):
|
|
72
|
+
...
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If you accidentally place `Annotated[...]` as a default (e.g. `jwt = Annotated[...]`), ClassApi attempts to normalize it so FastAPI recognizes the dependency. Still, annotate parameters properly when possible.
|
|
76
|
+
|
|
77
|
+
**Running the app**
|
|
78
|
+
- Use `uv` to run the app with reload during development:
|
|
79
|
+
```bash
|
|
80
|
+
uv run uvicorn tests.app_test.main:app --reload
|
|
81
|
+
```
|
|
82
|
+
or, if you use the helper script `test_init.py` at the repository root:
|
|
83
|
+
```bash
|
|
84
|
+
uv run .\test_init.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Debugging endpoint signatures**
|
|
88
|
+
If docs don't show expected parameters, you can inspect endpoint signatures at runtime:
|
|
89
|
+
```py
|
|
90
|
+
for r in app.routes:
|
|
91
|
+
print(r.path, getattr(r.endpoint, '__signature__', None))
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Editor integration (VSCode / Pylance)**
|
|
95
|
+
Pylance is a static analyzer and doesn't pick up runtime-generated signatures. To get editor hovers that match your runtime docs, create a `.pyi` stub next to your views module describing the public signatures (this does not change runtime behavior).
|
|
96
|
+
|
|
97
|
+
**Contributing**
|
|
98
|
+
- Open issues or PRs.
|
|
99
|
+
- Add tests under `tests/` and run them with `pytest`.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
If you want, I can generate a `views.pyi` stub for your views, add example tests, or add a minimal `pyproject.toml`. Which should I do next?
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import fastapi as _fastapi
|
|
2
|
+
|
|
3
|
+
class ClassApi(_fastapi.FastAPI):
|
|
4
|
+
def __init__(self, *args, **kwargs):
|
|
5
|
+
kwargs.setdefault("title", "ClassApi (FastAPI with class-based views)")
|
|
6
|
+
super().__init__(*args, **kwargs)
|
|
7
|
+
|
|
8
|
+
def include_routers(self, path_file, prefix: str = ""):
|
|
9
|
+
import os
|
|
10
|
+
import importlib
|
|
11
|
+
import inspect
|
|
12
|
+
#search the "list" url in the file and include it in the app
|
|
13
|
+
# Support three forms:
|
|
14
|
+
# - absolute module path: "package.module"
|
|
15
|
+
# - path with leading slash or slashes: "/package/module" -> "package.module"
|
|
16
|
+
# - relative module path starting with dot: ".src.urls" (resolved gainst caller package)
|
|
17
|
+
module = None
|
|
18
|
+
if path_file.startswith("/"):
|
|
19
|
+
module_path = path_file.lstrip("/").replace("/", ".")
|
|
20
|
+
module = importlib.import_module(module_path)
|
|
21
|
+
elif path_file.startswith("."):
|
|
22
|
+
# find caller package to perform relative import
|
|
23
|
+
frm = inspect.currentframe()
|
|
24
|
+
try:
|
|
25
|
+
caller = frm.f_back
|
|
26
|
+
package = None
|
|
27
|
+
if caller is not None:
|
|
28
|
+
package = caller.f_globals.get("__package__") or caller.f_globals.get("__name__")
|
|
29
|
+
if not package:
|
|
30
|
+
raise ImportError("cannot resolve relative import: caller package unknown")
|
|
31
|
+
module = importlib.import_module(path_file, package=package)
|
|
32
|
+
finally:
|
|
33
|
+
del frm
|
|
34
|
+
else:
|
|
35
|
+
module = importlib.import_module(path_file.replace("/", "."))
|
|
36
|
+
|
|
37
|
+
urls = getattr(module, "urls", [])
|
|
38
|
+
print(f"Included routes from {path_file}: {urls}")
|
|
39
|
+
for i in urls:
|
|
40
|
+
# support two formats for route entries:
|
|
41
|
+
# - dict: {"path": "/x", "view": ViewClass, ...fastapi kwargs...}
|
|
42
|
+
# - list/tuple: ["/x", ViewClass]
|
|
43
|
+
route_kwargs = {}
|
|
44
|
+
if isinstance(i, dict):
|
|
45
|
+
path = prefix + i.get("path", "")
|
|
46
|
+
view = i["view"]
|
|
47
|
+
# extra keys become kwargs forwarded to add_api_route
|
|
48
|
+
route_kwargs = {k: v for k, v in i.items() if k not in ("path", "view")}
|
|
49
|
+
elif isinstance(i, (list, tuple)) and len(i) >= 2:
|
|
50
|
+
path = prefix + i[0]
|
|
51
|
+
view = i[1]
|
|
52
|
+
else:
|
|
53
|
+
raise AssertionError("Route entries must be dict or (path, view) tuple/list")
|
|
54
|
+
|
|
55
|
+
assert issubclass(view, BaseView), f"View class {view} must inherit from View base class"
|
|
56
|
+
assert type(path) == str, f"Path must be a string, got {type(path)}"
|
|
57
|
+
print(f"Adding route: {path} -> {view} with {route_kwargs}")
|
|
58
|
+
view()._add_route(self, path=path, **route_kwargs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BaseView:
|
|
63
|
+
methods = []
|
|
64
|
+
|
|
65
|
+
def pre_process(self, *args, **kwargs):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def pre_get(self, *args, **kwargs):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def pre_post(self, *args, **kwargs):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def pre_put(self, *args, **kwargs):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def pre_delete(self, *args, **kwargs):
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def pre_patch(self, *args, **kwargs):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def _add_route(self, app: _fastapi.FastAPI, path: str = "/", **kwargs):
|
|
84
|
+
"""Register handlers on `app` forwarding any FastAPI-compatible kwargs.
|
|
85
|
+
|
|
86
|
+
`kwargs` are passed directly to `app.add_api_route`, so you can provide
|
|
87
|
+
`response_model`, `dependencies`, `status_code`, etc.
|
|
88
|
+
"""
|
|
89
|
+
# create FastAPI-compatible endpoint wrappers that preserve the
|
|
90
|
+
# original method signature (excluding `self`) by setting
|
|
91
|
+
# `__signature__` on the wrapper. FastAPI uses that for dependency
|
|
92
|
+
# injection and parameter parsing.
|
|
93
|
+
def _make_endpoint(method_name, http_methods):
|
|
94
|
+
import inspect
|
|
95
|
+
func = getattr(self, method_name)
|
|
96
|
+
sig_func = inspect.signature(func)
|
|
97
|
+
params_func = list(sig_func.parameters.values())
|
|
98
|
+
if params_func and params_func[0].name == "self":
|
|
99
|
+
params_func = params_func[1:]
|
|
100
|
+
# exclude *args and **kwargs from the public signature
|
|
101
|
+
params_func = [p for p in params_func if p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)]
|
|
102
|
+
|
|
103
|
+
# gather pre handlers: specific pre_<method> and general pre_process
|
|
104
|
+
pre_specific = getattr(self, f"pre_{method_name}", None)
|
|
105
|
+
pre_general = getattr(self, "pre_process", None)
|
|
106
|
+
|
|
107
|
+
params_pre_specific = []
|
|
108
|
+
params_pre_general = []
|
|
109
|
+
if pre_specific is not None:
|
|
110
|
+
sig_pre = inspect.signature(pre_specific)
|
|
111
|
+
params_pre_specific = list(sig_pre.parameters.values())
|
|
112
|
+
if params_pre_specific and params_pre_specific[0].name == "self":
|
|
113
|
+
params_pre_specific = params_pre_specific[1:]
|
|
114
|
+
# exclude *args and **kwargs from the public signature
|
|
115
|
+
params_pre_specific = [p for p in params_pre_specific if p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)]
|
|
116
|
+
# also filter accidental plain 'args'/'kwargs' parameters
|
|
117
|
+
params_pre_specific = [p for p in params_pre_specific if p.name not in ("args", "kwargs")]
|
|
118
|
+
if pre_general is not None:
|
|
119
|
+
sig_pre_g = inspect.signature(pre_general)
|
|
120
|
+
params_pre_general = list(sig_pre_g.parameters.values())
|
|
121
|
+
if params_pre_general and params_pre_general[0].name == "self":
|
|
122
|
+
params_pre_general = params_pre_general[1:]
|
|
123
|
+
# exclude *args and **kwargs from the public signature
|
|
124
|
+
params_pre_general = [p for p in params_pre_general if p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)]
|
|
125
|
+
# also filter accidental plain 'args'/'kwargs' parameters
|
|
126
|
+
params_pre_general = [p for p in params_pre_general if p.name not in ("args", "kwargs")]
|
|
127
|
+
|
|
128
|
+
# merge params: general pre, specific pre, then func params; skip duplicates by name
|
|
129
|
+
seen = set()
|
|
130
|
+
merged = []
|
|
131
|
+
for p in params_pre_general + params_pre_specific + params_func:
|
|
132
|
+
if p.name not in seen:
|
|
133
|
+
merged.append(p)
|
|
134
|
+
seen.add(p.name)
|
|
135
|
+
|
|
136
|
+
# Reorder merged params to satisfy Python's parameter ordering rules:
|
|
137
|
+
# positional-only, positional-or-keyword, var-positional, keyword-only, var-keyword
|
|
138
|
+
posonly = []
|
|
139
|
+
pos_or_kw = []
|
|
140
|
+
var_pos = None
|
|
141
|
+
kw_only = []
|
|
142
|
+
var_kw = None
|
|
143
|
+
for p in merged:
|
|
144
|
+
if p.kind == inspect.Parameter.POSITIONAL_ONLY:
|
|
145
|
+
posonly.append(p)
|
|
146
|
+
elif p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
|
147
|
+
pos_or_kw.append(p)
|
|
148
|
+
elif p.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
149
|
+
if var_pos is None:
|
|
150
|
+
var_pos = p
|
|
151
|
+
elif p.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
152
|
+
kw_only.append(p)
|
|
153
|
+
elif p.kind == inspect.Parameter.VAR_KEYWORD:
|
|
154
|
+
if var_kw is None:
|
|
155
|
+
var_kw = p
|
|
156
|
+
|
|
157
|
+
# Ensure parameters without defaults come before those with defaults
|
|
158
|
+
def split_by_default(params):
|
|
159
|
+
no_def = [p for p in params if p.default is inspect._empty]
|
|
160
|
+
has_def = [p for p in params if p.default is not inspect._empty]
|
|
161
|
+
return no_def + has_def
|
|
162
|
+
|
|
163
|
+
posonly = split_by_default(posonly)
|
|
164
|
+
pos_or_kw = split_by_default(pos_or_kw)
|
|
165
|
+
kw_only = split_by_default(kw_only)
|
|
166
|
+
|
|
167
|
+
ordered = posonly + pos_or_kw
|
|
168
|
+
if var_pos is not None:
|
|
169
|
+
ordered.append(var_pos)
|
|
170
|
+
ordered += kw_only
|
|
171
|
+
if var_kw is not None:
|
|
172
|
+
ordered.append(var_kw)
|
|
173
|
+
|
|
174
|
+
# normalize parameters: if a parameter has no annotation but its default
|
|
175
|
+
# is an `Annotated[T, metadata...]` object (commonly used incorrectly
|
|
176
|
+
# as a default), extract the annotation and the first metadata item
|
|
177
|
+
# (e.g. Header()) and set them on the parameter so FastAPI detects it.
|
|
178
|
+
import typing
|
|
179
|
+
normalized = []
|
|
180
|
+
for p in ordered:
|
|
181
|
+
ann = p.annotation
|
|
182
|
+
default = p.default
|
|
183
|
+
if (ann is inspect._empty and default is not inspect._empty
|
|
184
|
+
and typing.get_origin(default) is typing.Annotated):
|
|
185
|
+
args = typing.get_args(default)
|
|
186
|
+
if args:
|
|
187
|
+
new_ann = args[0]
|
|
188
|
+
new_default = args[1] if len(args) > 1 else inspect._empty
|
|
189
|
+
p = inspect.Parameter(p.name, p.kind, default=new_default, annotation=new_ann)
|
|
190
|
+
normalized.append(p)
|
|
191
|
+
|
|
192
|
+
new_sig = inspect.Signature(parameters=normalized)
|
|
193
|
+
|
|
194
|
+
def endpoint(**kwargs):
|
|
195
|
+
# call general pre_process with its subset
|
|
196
|
+
if pre_general is not None:
|
|
197
|
+
pre_g_kwargs = {k: v for k, v in kwargs.items() if k in {p.name for p in params_pre_general}}
|
|
198
|
+
pre_general(**pre_g_kwargs)
|
|
199
|
+
# call specific pre_<method> with its subset
|
|
200
|
+
if pre_specific is not None:
|
|
201
|
+
pre_s_kwargs = {k: v for k, v in kwargs.items() if k in {p.name for p in params_pre_specific}}
|
|
202
|
+
pre_specific(**pre_s_kwargs)
|
|
203
|
+
# call actual handler with its subset
|
|
204
|
+
func_kwargs = {k: v for k, v in kwargs.items() if k in {p.name for p in params_func}}
|
|
205
|
+
return func(**func_kwargs)
|
|
206
|
+
|
|
207
|
+
endpoint.__signature__ = new_sig
|
|
208
|
+
endpoint.__name__ = f"{self.__class__.__name__}_{method_name}_endpoint"
|
|
209
|
+
return endpoint
|
|
210
|
+
|
|
211
|
+
if hasattr(self, "get") and ("GET" in self.methods or "get" in self.methods):
|
|
212
|
+
app.add_api_route(path, _make_endpoint("get", ["GET"]), methods=["GET"], **kwargs)
|
|
213
|
+
if hasattr(self, "post") and ("POST" in self.methods or "post" in self.methods):
|
|
214
|
+
app.add_api_route(path, _make_endpoint("post", ["POST"]), methods=["POST"], **kwargs)
|
|
215
|
+
if hasattr(self, "put") and ("PUT" in self.methods or "put" in self.methods):
|
|
216
|
+
app.add_api_route(path, _make_endpoint("put", ["PUT"]), methods=["PUT"], **kwargs)
|
|
217
|
+
if hasattr(self, "delete") and ("DELETE" in self.methods or "delete" in self.methods):
|
|
218
|
+
app.add_api_route(path, _make_endpoint("delete", ["DELETE"]), methods=["DELETE"], **kwargs)
|
|
219
|
+
if hasattr(self, "patch") and ("PATCH" in self.methods or "patch" in self.methods):
|
|
220
|
+
app.add_api_route(path, _make_endpoint("patch", ["PATCH"]), methods=["PATCH"], **kwargs)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _post(self, *args, **kwargs):
|
|
224
|
+
self.pre_process(*args, **kwargs)
|
|
225
|
+
self.pre_post(*args, **kwargs)
|
|
226
|
+
return self.post(*args, **kwargs)
|
|
227
|
+
|
|
228
|
+
def _get(self, *args, **kwargs):
|
|
229
|
+
self.pre_process(*args, **kwargs)
|
|
230
|
+
self.pre_get(*args, **kwargs)
|
|
231
|
+
return self.get(*args, **kwargs)
|
|
232
|
+
|
|
233
|
+
def _put(self, *args, **kwargs):
|
|
234
|
+
self.pre_process(*args, **kwargs)
|
|
235
|
+
self.pre_put(*args, **kwargs)
|
|
236
|
+
return self.put(*args, **kwargs)
|
|
237
|
+
|
|
238
|
+
def _delete(self, *args, **kwargs):
|
|
239
|
+
self.pre_process(*args, **kwargs)
|
|
240
|
+
self.pre_delete(*args, **kwargs)
|
|
241
|
+
return self.delete(*args, **kwargs)
|
|
242
|
+
|
|
243
|
+
def _patch(self, *args, **kwargs):
|
|
244
|
+
self.pre_process(*args, **kwargs)
|
|
245
|
+
self.pre_patch(*args, **kwargs)
|
|
246
|
+
return self.patch(*args, **kwargs)
|
|
247
|
+
|
|
248
|
+
def post(self, *args):
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
def get(self, *args):
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
def put(self, *args):
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
def delete(self, *args):
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
def patch(self, *args):
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
def include(directory: str):
|
|
264
|
+
import os
|
|
265
|
+
import importlib
|
|
266
|
+
#search the "list" url in the file and include it in the app
|
|
267
|
+
for file in os.listdir(directory):
|
|
268
|
+
if file.endswith(".py") and file != "__init__.py":
|
|
269
|
+
module_name = file[:-3]
|
|
270
|
+
module = importlib.import_module(f"{directory.replace('/', '.')}.{module_name}")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: classapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastapi[standard]>=0.129.0
|
|
8
|
+
|
|
9
|
+
# ClassApi
|
|
10
|
+
ClassApi is a small convenience layer on top of FastAPI that enables class-based views (CBV). It preserves FastAPI's features (typing, dependencies, automatic docs) while letting you organize handlers as classes.
|
|
11
|
+
|
|
12
|
+
**Key features**
|
|
13
|
+
- Use `BaseView` as a base class for handlers (`get`, `post`, `put`, ...).
|
|
14
|
+
- Support for `pre_process` and `pre_<method>` hooks to validate or transform requests.
|
|
15
|
+
- Register route modules by module path (supports relative module paths like `.src.urls`).
|
|
16
|
+
- Combined signatures from `pre_process` and the handler method are exposed to FastAPI for documentation and form generation.
|
|
17
|
+
|
|
18
|
+
**Development setup (using `uv` helper)**
|
|
19
|
+
1. Create a virtual environment:
|
|
20
|
+
```bash
|
|
21
|
+
python -m venv .venv
|
|
22
|
+
```
|
|
23
|
+
2. Use your `uv` helper to run `pip` inside the project environment and install dependencies:
|
|
24
|
+
```bash
|
|
25
|
+
uv run pip install fastapi uvicorn
|
|
26
|
+
# or install from requirements: uv run pip install -r requirements.txt
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Quickstart**
|
|
30
|
+
Create `main.py`:
|
|
31
|
+
```py
|
|
32
|
+
from classapi import ClassApi
|
|
33
|
+
|
|
34
|
+
app = ClassApi()
|
|
35
|
+
|
|
36
|
+
app.include_routers(".src.urls")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Example routes and views layout (tests/app_test/src):
|
|
40
|
+
```py
|
|
41
|
+
# tests/app_test/src/urls.py
|
|
42
|
+
from .views import HelloWorldView
|
|
43
|
+
|
|
44
|
+
urls = [
|
|
45
|
+
{"path": "/hello", "view": HelloWorldView}
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# tests/app_test/src/views.py
|
|
49
|
+
from classapi import View, Header, HTTPException
|
|
50
|
+
from typing import Annotated
|
|
51
|
+
|
|
52
|
+
class ValidateUser(BaseView):
|
|
53
|
+
def pre_process(self, jwt: Annotated[str | None, Header()] = None):
|
|
54
|
+
if jwt != "valid_jwt":
|
|
55
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
|
56
|
+
|
|
57
|
+
class HelloWorldView(ValidateUser, BaseView):
|
|
58
|
+
methods = ["GET"]
|
|
59
|
+
|
|
60
|
+
def get(self, name: str = "World"):
|
|
61
|
+
return {"Hello": name}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Supported `urls` formats**
|
|
65
|
+
- Dict entries: `{"path": "/x", "view": MyView, ...fastapi kwargs...}` — extra kwargs (e.g. `response_model`) are forwarded to `add_api_route`.
|
|
66
|
+
- Tuple/list entries: `("/x", MyView)`.
|
|
67
|
+
- You may use relative imports from the calling module: `app.include_routers(".src.urls")`.
|
|
68
|
+
|
|
69
|
+
**`View` classes**
|
|
70
|
+
- Define HTTP methods: `get`, `post`, `put`, `delete`, `patch`.
|
|
71
|
+
- Limit exposed methods with `methods = ["GET"]` on the class.
|
|
72
|
+
- Hooks:
|
|
73
|
+
- `pre_process(self, ...)` — runs before any handler.
|
|
74
|
+
- `pre_get(self, ...)`, `pre_post(...)`, ... — run before a specific handler.
|
|
75
|
+
- Signatures from `pre_process`, `pre_<method>` and the handler itself are merged and exposed to FastAPI; annotate parameters with `Annotated[..., Header()]`, `Cookie()`, etc., to appear correctly in `/docs`.
|
|
76
|
+
|
|
77
|
+
Example: header extraction in `pre_process`:
|
|
78
|
+
```py
|
|
79
|
+
def pre_process(self, jwt: Annotated[str|None, Header()] = None):
|
|
80
|
+
...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If you accidentally place `Annotated[...]` as a default (e.g. `jwt = Annotated[...]`), ClassApi attempts to normalize it so FastAPI recognizes the dependency. Still, annotate parameters properly when possible.
|
|
84
|
+
|
|
85
|
+
**Running the app**
|
|
86
|
+
- Use `uv` to run the app with reload during development:
|
|
87
|
+
```bash
|
|
88
|
+
uv run uvicorn tests.app_test.main:app --reload
|
|
89
|
+
```
|
|
90
|
+
or, if you use the helper script `test_init.py` at the repository root:
|
|
91
|
+
```bash
|
|
92
|
+
uv run .\test_init.py
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Debugging endpoint signatures**
|
|
96
|
+
If docs don't show expected parameters, you can inspect endpoint signatures at runtime:
|
|
97
|
+
```py
|
|
98
|
+
for r in app.routes:
|
|
99
|
+
print(r.path, getattr(r.endpoint, '__signature__', None))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Editor integration (VSCode / Pylance)**
|
|
103
|
+
Pylance is a static analyzer and doesn't pick up runtime-generated signatures. To get editor hovers that match your runtime docs, create a `.pyi` stub next to your views module describing the public signatures (this does not change runtime behavior).
|
|
104
|
+
|
|
105
|
+
**Contributing**
|
|
106
|
+
- Open issues or PRs.
|
|
107
|
+
- Add tests under `tests/` and run them with `pytest`.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
If you want, I can generate a `views.pyi` stub for your views, add example tests, or add a minimal `pyproject.toml`. Which should I do next?
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastapi[standard]>=0.129.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
classapi
|
classapi-0.1.0/setup.cfg
ADDED