sqlmodelclassapi 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
File without changes
|
sqlmodelclassapi/main.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from classapi import BaseView
|
|
2
|
+
import fastapi as _fastapi
|
|
3
|
+
from sqlmodel import Session
|
|
4
|
+
|
|
5
|
+
def make_session_view(engine = None):
|
|
6
|
+
class SessionView(BaseView):
|
|
7
|
+
def __init__(self):
|
|
8
|
+
super().__init__()
|
|
9
|
+
self.session = None # will be set in the endpoint wrapper
|
|
10
|
+
|
|
11
|
+
def _add_route(self, app: _fastapi.FastAPI, path: str = "/", **kwargs):
|
|
12
|
+
"""Register handlers on `app` forwarding any FastAPI-compatible kwargs.
|
|
13
|
+
|
|
14
|
+
`kwargs` are passed directly to `app.add_api_route`, so you can provide
|
|
15
|
+
`response_model`, `dependencies`, `status_code`, etc.
|
|
16
|
+
"""
|
|
17
|
+
# create FastAPI-compatible endpoint wrappers that preserve the
|
|
18
|
+
# original method signature (excluding `self`) by setting
|
|
19
|
+
# `__signature__` on the wrapper. FastAPI uses that for dependency
|
|
20
|
+
# injection and parameter parsing.
|
|
21
|
+
def _make_endpoint(method_name, http_methods):
|
|
22
|
+
import inspect
|
|
23
|
+
func = getattr(self, method_name)
|
|
24
|
+
sig_func = inspect.signature(func)
|
|
25
|
+
params_func = list(sig_func.parameters.values())
|
|
26
|
+
if params_func and params_func[0].name == "self":
|
|
27
|
+
params_func = params_func[1:]
|
|
28
|
+
# exclude *args and **kwargs from the public signature
|
|
29
|
+
params_func = [p for p in params_func if p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)]
|
|
30
|
+
|
|
31
|
+
# gather pre handlers: specific pre_<method> and general pre_process
|
|
32
|
+
pre_specific = getattr(self, f"pre_{method_name}", None)
|
|
33
|
+
pre_general = getattr(self, "pre_process", None)
|
|
34
|
+
|
|
35
|
+
params_pre_specific = []
|
|
36
|
+
params_pre_general = []
|
|
37
|
+
if pre_specific is not None:
|
|
38
|
+
sig_pre = inspect.signature(pre_specific)
|
|
39
|
+
params_pre_specific = list(sig_pre.parameters.values())
|
|
40
|
+
if params_pre_specific and params_pre_specific[0].name == "self":
|
|
41
|
+
params_pre_specific = params_pre_specific[1:]
|
|
42
|
+
# exclude *args and **kwargs from the public signature
|
|
43
|
+
params_pre_specific = [p for p in params_pre_specific if p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)]
|
|
44
|
+
# also filter accidental plain 'args'/'kwargs' parameters
|
|
45
|
+
params_pre_specific = [p for p in params_pre_specific if p.name not in ("args", "kwargs")]
|
|
46
|
+
if pre_general is not None:
|
|
47
|
+
sig_pre_g = inspect.signature(pre_general)
|
|
48
|
+
params_pre_general = list(sig_pre_g.parameters.values())
|
|
49
|
+
if params_pre_general and params_pre_general[0].name == "self":
|
|
50
|
+
params_pre_general = params_pre_general[1:]
|
|
51
|
+
# exclude *args and **kwargs from the public signature
|
|
52
|
+
params_pre_general = [p for p in params_pre_general if p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)]
|
|
53
|
+
# also filter accidental plain 'args'/'kwargs' parameters
|
|
54
|
+
params_pre_general = [p for p in params_pre_general if p.name not in ("args", "kwargs")]
|
|
55
|
+
|
|
56
|
+
# merge params: general pre, specific pre, then func params; skip duplicates by name
|
|
57
|
+
seen = set()
|
|
58
|
+
merged = []
|
|
59
|
+
for p in params_pre_general + params_pre_specific + params_func:
|
|
60
|
+
if p.name not in seen:
|
|
61
|
+
merged.append(p)
|
|
62
|
+
seen.add(p.name)
|
|
63
|
+
|
|
64
|
+
# Reorder merged params to satisfy Python's parameter ordering rules:
|
|
65
|
+
# positional-only, positional-or-keyword, var-positional, keyword-only, var-keyword
|
|
66
|
+
posonly = []
|
|
67
|
+
pos_or_kw = []
|
|
68
|
+
var_pos = None
|
|
69
|
+
kw_only = []
|
|
70
|
+
var_kw = None
|
|
71
|
+
for p in merged:
|
|
72
|
+
if p.kind == inspect.Parameter.POSITIONAL_ONLY:
|
|
73
|
+
posonly.append(p)
|
|
74
|
+
elif p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
|
75
|
+
pos_or_kw.append(p)
|
|
76
|
+
elif p.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
77
|
+
if var_pos is None:
|
|
78
|
+
var_pos = p
|
|
79
|
+
elif p.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
80
|
+
kw_only.append(p)
|
|
81
|
+
elif p.kind == inspect.Parameter.VAR_KEYWORD:
|
|
82
|
+
if var_kw is None:
|
|
83
|
+
var_kw = p
|
|
84
|
+
|
|
85
|
+
# Ensure parameters without defaults come before those with defaults
|
|
86
|
+
def split_by_default(params):
|
|
87
|
+
no_def = [p for p in params if p.default is inspect._empty]
|
|
88
|
+
has_def = [p for p in params if p.default is not inspect._empty]
|
|
89
|
+
return no_def + has_def
|
|
90
|
+
|
|
91
|
+
posonly = split_by_default(posonly)
|
|
92
|
+
pos_or_kw = split_by_default(pos_or_kw)
|
|
93
|
+
kw_only = split_by_default(kw_only)
|
|
94
|
+
|
|
95
|
+
ordered = posonly + pos_or_kw
|
|
96
|
+
if var_pos is not None:
|
|
97
|
+
ordered.append(var_pos)
|
|
98
|
+
ordered += kw_only
|
|
99
|
+
if var_kw is not None:
|
|
100
|
+
ordered.append(var_kw)
|
|
101
|
+
|
|
102
|
+
# normalize parameters: if a parameter has no annotation but its default
|
|
103
|
+
# is an `Annotated[T, metadata...]` object (commonly used incorrectly
|
|
104
|
+
# as a default), extract the annotation and the first metadata item
|
|
105
|
+
# (e.g. Header()) and set them on the parameter so FastAPI detects it.
|
|
106
|
+
import typing
|
|
107
|
+
normalized = []
|
|
108
|
+
for p in ordered:
|
|
109
|
+
ann = p.annotation
|
|
110
|
+
default = p.default
|
|
111
|
+
if (ann is inspect._empty and default is not inspect._empty
|
|
112
|
+
and typing.get_origin(default) is typing.Annotated):
|
|
113
|
+
args = typing.get_args(default)
|
|
114
|
+
if args:
|
|
115
|
+
new_ann = args[0]
|
|
116
|
+
new_default = args[1] if len(args) > 1 else inspect._empty
|
|
117
|
+
p = inspect.Parameter(p.name, p.kind, default=new_default, annotation=new_ann)
|
|
118
|
+
normalized.append(p)
|
|
119
|
+
|
|
120
|
+
new_sig = inspect.Signature(parameters=normalized)
|
|
121
|
+
|
|
122
|
+
def endpoint(**kwargs):
|
|
123
|
+
if engine is None:
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
"SessionView has no engine configured. "
|
|
126
|
+
"Create it with make_session_view(engine=your_engine)."
|
|
127
|
+
)
|
|
128
|
+
with Session(engine) as session:
|
|
129
|
+
self.session = session # make session available as an instance attribute for handlers and pre-processors
|
|
130
|
+
try:
|
|
131
|
+
# call general pre_process with its subset
|
|
132
|
+
if pre_general is not None:
|
|
133
|
+
pre_g_kwargs = {k: v for k, v in kwargs.items() if k in {p.name for p in params_pre_general}}
|
|
134
|
+
pre_general(**pre_g_kwargs)
|
|
135
|
+
# call specific pre_<method> with its subset
|
|
136
|
+
if pre_specific is not None:
|
|
137
|
+
pre_s_kwargs = {k: v for k, v in kwargs.items() if k in {p.name for p in params_pre_specific}}
|
|
138
|
+
pre_specific(**pre_s_kwargs)
|
|
139
|
+
# call actual handler with its subset
|
|
140
|
+
func_kwargs = {k: v for k, v in kwargs.items() if k in {p.name for p in params_func}}
|
|
141
|
+
return func(**func_kwargs)
|
|
142
|
+
finally:
|
|
143
|
+
self.session = None
|
|
144
|
+
|
|
145
|
+
endpoint.__signature__ = new_sig
|
|
146
|
+
endpoint.__name__ = f"{self.__class__.__name__}_{method_name}_endpoint"
|
|
147
|
+
return endpoint
|
|
148
|
+
|
|
149
|
+
if hasattr(self, "get") and ("GET" in self.methods or "get" in self.methods):
|
|
150
|
+
app.add_api_route(path, _make_endpoint("get", ["GET"]), methods=["GET"], **kwargs)
|
|
151
|
+
if hasattr(self, "post") and ("POST" in self.methods or "post" in self.methods):
|
|
152
|
+
app.add_api_route(path, _make_endpoint("post", ["POST"]), methods=["POST"], **kwargs)
|
|
153
|
+
if hasattr(self, "put") and ("PUT" in self.methods or "put" in self.methods):
|
|
154
|
+
app.add_api_route(path, _make_endpoint("put", ["PUT"]), methods=["PUT"], **kwargs)
|
|
155
|
+
if hasattr(self, "delete") and ("DELETE" in self.methods or "delete" in self.methods):
|
|
156
|
+
app.add_api_route(path, _make_endpoint("delete", ["DELETE"]), methods=["DELETE"], **kwargs)
|
|
157
|
+
if hasattr(self, "patch") and ("PATCH" in self.methods or "patch" in self.methods):
|
|
158
|
+
app.add_api_route(path, _make_endpoint("patch", ["PATCH"]), methods=["PATCH"], **kwargs)
|
|
159
|
+
|
|
160
|
+
return SessionView
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlmodelclassapi
|
|
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: classapi>=0.1.0.1
|
|
8
|
+
Requires-Dist: sqlmodel>=0.0.34
|
|
9
|
+
|
|
10
|
+
# SQLModel support for classApi
|
|
11
|
+
|
|
12
|
+
This project provides a simple way to use a SQLModel `Session` inside your classApi views.
|
|
13
|
+
|
|
14
|
+
First, create your SQLModel engine. Here is a basic example:
|
|
15
|
+
|
|
16
|
+
```py
|
|
17
|
+
# engine.py
|
|
18
|
+
from sqlalchemy import create_engine
|
|
19
|
+
from sqlmodel import SQLModel
|
|
20
|
+
|
|
21
|
+
from .model import User # In this example, User has 3 fields: id, name, and email
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
sqlite_file_name = "database.db"
|
|
25
|
+
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
|
26
|
+
|
|
27
|
+
engine = create_engine(sqlite_url, echo=True)
|
|
28
|
+
|
|
29
|
+
def create_db_and_tables():
|
|
30
|
+
SQLModel.metadata.create_all(engine)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then create a new base view using `make_session_view`:
|
|
34
|
+
|
|
35
|
+
```py
|
|
36
|
+
# engine.py
|
|
37
|
+
from sqlalchemy import create_engine
|
|
38
|
+
from sqlmodel import SQLModel
|
|
39
|
+
|
|
40
|
+
from sqlmodelclassapi.main import make_session_view
|
|
41
|
+
from .model import User
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
sqlite_file_name = "database.db"
|
|
45
|
+
sqlite_url = f"sqlite:///{sqlite_file_name}"
|
|
46
|
+
|
|
47
|
+
engine = create_engine(sqlite_url, echo=True)
|
|
48
|
+
|
|
49
|
+
def create_db_and_tables():
|
|
50
|
+
SQLModel.metadata.create_all(engine)
|
|
51
|
+
|
|
52
|
+
SessionView = make_session_view(engine=engine) # New base view with session support
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Now, to create a view, inherit from `SessionView` instead of `BaseView`:
|
|
56
|
+
|
|
57
|
+
```py
|
|
58
|
+
# views.py
|
|
59
|
+
class ExampleSessionView(SessionView):
|
|
60
|
+
methods = ["GET", "POST"]
|
|
61
|
+
|
|
62
|
+
def get(self, *args):
|
|
63
|
+
statement = select(User)
|
|
64
|
+
results = self.session.exec(statement).all()
|
|
65
|
+
return [
|
|
66
|
+
{"id": user.id, "name": user.name, "email": user.email}
|
|
67
|
+
for user in results
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
def post(self, name: str):
|
|
71
|
+
new_user = User(name=name, email=f"{name.lower()}@example.com")
|
|
72
|
+
self.session.add(new_user)
|
|
73
|
+
self.session.commit()
|
|
74
|
+
self.session.refresh(new_user)
|
|
75
|
+
|
|
76
|
+
return {"message": f"User '{name}' created successfully!", "user": new_user}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
With this setup, all operations in your request run inside the same session, including `pre_{method}` hooks.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
sqlmodelclassapi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
sqlmodelclassapi/main.py,sha256=TT8wv25l58E1XxucEbjQbYZsUJYVgGpd3FMtEj5e-14,9348
|
|
3
|
+
sqlmodelclassapi-0.1.0.dist-info/METADATA,sha256=bNv-QR7VZI5ZQgF5QGMNyEQOVOBPwoJlmF-rs6Jilow,2176
|
|
4
|
+
sqlmodelclassapi-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
5
|
+
sqlmodelclassapi-0.1.0.dist-info/top_level.txt,sha256=HI1EUMdAJr7kYZCahTAk9haV4MmJRyoe1KlIBUO1HWw,17
|
|
6
|
+
sqlmodelclassapi-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sqlmodelclassapi
|