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.
@@ -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,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,2 @@
1
+ from .classapi import ClassApi, BaseView
2
+ from fastapi import *
@@ -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,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ classapi/__init__.py
4
+ classapi/classapi.py
5
+ classapi.egg-info/PKG-INFO
6
+ classapi.egg-info/SOURCES.txt
7
+ classapi.egg-info/dependency_links.txt
8
+ classapi.egg-info/requires.txt
9
+ classapi.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ fastapi[standard]>=0.129.0
@@ -0,0 +1 @@
1
+ classapi
@@ -0,0 +1,9 @@
1
+ [project]
2
+ name = "classapi"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "fastapi[standard]>=0.129.0",
9
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+