sqlalchemy-studio 0.1.8__tar.gz → 0.1.9__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.
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/PKG-INFO +2 -1
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/pyproject.toml +9 -1
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio/Studio.py +158 -62
- sqlalchemy_studio-0.1.9/sqlalchemy_studio/backend.py +106 -0
- sqlalchemy_studio-0.1.9/sqlalchemy_studio/static/assets/index-D9EiKPre.css +2 -0
- sqlalchemy_studio-0.1.8/sqlalchemy_studio/static/assets/index-0qQM-QLq.js → sqlalchemy_studio-0.1.9/sqlalchemy_studio/static/assets/index-OOWfHZW5.js +1 -1
- sqlalchemy_studio-0.1.8/sqlalchemy_studio/static/assets/routes-BJJUUmei.js → sqlalchemy_studio-0.1.9/sqlalchemy_studio/static/assets/routes-BhTC3gEq.js +1 -1
- sqlalchemy_studio-0.1.9/sqlalchemy_studio/static/assets/tables._tableName-C-AKBKP7.js +1 -0
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio/static/index.html +2 -2
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio.egg-info/PKG-INFO +2 -1
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio.egg-info/SOURCES.txt +4 -4
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio.egg-info/requires.txt +1 -0
- sqlalchemy_studio-0.1.8/sqlalchemy_studio/backend.py +0 -68
- sqlalchemy_studio-0.1.8/sqlalchemy_studio/static/assets/index-B8xFyRBc.css +0 -2
- sqlalchemy_studio-0.1.8/sqlalchemy_studio/static/assets/tables._tableName-C7FBzk6i.js +0 -1
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/README.md +0 -0
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/setup.cfg +0 -0
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio/__init__.py +0 -0
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio/static/favicon.ico +0 -0
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio/static/logo192.png +0 -0
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio/static/logo512.png +0 -0
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio/static/manifest.json +0 -0
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio/static/robots.txt +0 -0
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_studio-0.1.8 → sqlalchemy_studio-0.1.9}/sqlalchemy_studio.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-studio
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: FastAPI studio for inspecting SQLAlchemy databases
|
|
5
5
|
Author-email: Xursand Qarlibayev <coderxuz2009@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -18,6 +18,7 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
Requires-Dist: build>=1.5.0
|
|
19
19
|
Requires-Dist: fastapi>=0.136.3
|
|
20
20
|
Requires-Dist: sqlalchemy>=2.0.50
|
|
21
|
+
Requires-Dist: taskipy>=1.14.1
|
|
21
22
|
Requires-Dist: twine>=6.2.0
|
|
22
23
|
Requires-Dist: uvicorn>=0.48.0
|
|
23
24
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlalchemy-studio"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
8
8
|
description = "FastAPI studio for inspecting SQLAlchemy databases"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -12,6 +12,7 @@ dependencies = [
|
|
|
12
12
|
"build>=1.5.0",
|
|
13
13
|
"fastapi>=0.136.3",
|
|
14
14
|
"sqlalchemy>=2.0.50",
|
|
15
|
+
"taskipy>=1.14.1",
|
|
15
16
|
"twine>=6.2.0",
|
|
16
17
|
"uvicorn>=0.48.0",
|
|
17
18
|
]
|
|
@@ -35,3 +36,10 @@ include = ["studio*", "sqlalchemy_studio*"]
|
|
|
35
36
|
|
|
36
37
|
[tool.setuptools.package-data]
|
|
37
38
|
sqlalchemy_studio = ["static/**/*"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
[tool.taskipy.tasks]
|
|
42
|
+
build = "build"
|
|
43
|
+
upload = "-m twine upload dist/*"
|
|
44
|
+
dev = "uvicorn backend.main:app --reload"
|
|
45
|
+
bot = "python -m bot.main"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
from sqlalchemy import and_, create_engine, func, inspect, Engine, or_, select, Table
|
|
1
|
+
from sqlalchemy import and_, create_engine, delete, func, inspect, Engine, or_, select, Table
|
|
2
2
|
from sqlalchemy.engine.interfaces import ReflectedColumn
|
|
3
3
|
from sqlalchemy.orm import DeclarativeBase
|
|
4
|
+
from sqlalchemy.sql.elements import ColumnElement
|
|
4
5
|
from fastapi import FastAPI, HTTPException, status, Request
|
|
5
6
|
from fastapi.middleware.cors import CORSMiddleware
|
|
6
7
|
from fastapi.staticfiles import StaticFiles
|
|
@@ -15,7 +16,12 @@ from sqlalchemy_studio.backend import create_tables_router
|
|
|
15
16
|
from typing import Self, TypedDict, Any, cast, TYPE_CHECKING
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
|
-
from sqlalchemy_studio.backend import
|
|
19
|
+
from sqlalchemy_studio.backend import (
|
|
20
|
+
ColumnValueMatch,
|
|
21
|
+
DeleteRowsRequest,
|
|
22
|
+
FilterCondition,
|
|
23
|
+
FilterRequest,
|
|
24
|
+
)
|
|
19
25
|
|
|
20
26
|
engine = create_engine("sqlite:///test.db")
|
|
21
27
|
|
|
@@ -44,6 +50,10 @@ class GetRowsResponse(TypedDict):
|
|
|
44
50
|
total_count: int
|
|
45
51
|
|
|
46
52
|
|
|
53
|
+
class DeleteRowsResponse(TypedDict):
|
|
54
|
+
deleted_count: int
|
|
55
|
+
|
|
56
|
+
|
|
47
57
|
class Studio:
|
|
48
58
|
def __init__(self: Self, engine: Engine, base: DeclarativeBase) -> None:
|
|
49
59
|
self.engine = engine
|
|
@@ -174,84 +184,170 @@ class Studio:
|
|
|
174
184
|
|
|
175
185
|
return response_data
|
|
176
186
|
|
|
177
|
-
def
|
|
178
|
-
self: Self, table_name: str, payload: "FilterRequest"
|
|
179
|
-
) -> GetRowsResponse:
|
|
180
|
-
"""
|
|
181
|
-
Advanced filtering using logical operator trees matching custom UI filters.
|
|
182
|
-
"""
|
|
187
|
+
def _get_table_or_404(self: Self, table_name: str) -> Table:
|
|
183
188
|
if not self.inspector.has_table(table_name):
|
|
184
189
|
raise HTTPException(
|
|
185
|
-
status_code=
|
|
190
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
191
|
+
detail=f"Table '{table_name}' not found.",
|
|
186
192
|
)
|
|
187
193
|
|
|
188
|
-
|
|
194
|
+
return Table(
|
|
189
195
|
table_name,
|
|
190
196
|
self.base.metadata,
|
|
191
197
|
autoload_with=self.engine,
|
|
192
198
|
extend_existing=True,
|
|
193
199
|
)
|
|
194
200
|
|
|
201
|
+
@staticmethod
|
|
202
|
+
def _get_column_or_400(table: Table, column_name: str) -> Any:
|
|
203
|
+
if column_name not in table.c:
|
|
204
|
+
raise HTTPException(
|
|
205
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
206
|
+
detail=f"Column '{column_name}' does not exist on table '{table.name}'.",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return table.c[column_name]
|
|
210
|
+
|
|
211
|
+
def _build_filter_expression(
|
|
212
|
+
self: Self, table: Table, condition: "FilterCondition"
|
|
213
|
+
) -> ColumnElement[bool]:
|
|
214
|
+
column = self._get_column_or_400(table, condition.column)
|
|
215
|
+
value = condition.value
|
|
216
|
+
|
|
217
|
+
if condition.operator == "equals":
|
|
218
|
+
expression = column == value
|
|
219
|
+
elif condition.operator == "not_equals":
|
|
220
|
+
expression = column != value
|
|
221
|
+
elif condition.operator == "contains":
|
|
222
|
+
expression = column.ilike(f"%{value}%")
|
|
223
|
+
elif condition.operator == "starts_with":
|
|
224
|
+
expression = column.ilike(f"{value}%")
|
|
225
|
+
elif condition.operator == "ends_with":
|
|
226
|
+
expression = column.ilike(f"%{value}")
|
|
227
|
+
elif condition.operator == "greater_than":
|
|
228
|
+
expression = column > value
|
|
229
|
+
elif condition.operator == "less_than":
|
|
230
|
+
expression = column < value
|
|
231
|
+
elif condition.operator == "greater_than_or_equals":
|
|
232
|
+
expression = column >= value
|
|
233
|
+
elif condition.operator == "less_than_or_equals":
|
|
234
|
+
expression = column <= value
|
|
235
|
+
else:
|
|
236
|
+
raise HTTPException(
|
|
237
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
238
|
+
detail=f"Unsupported filter operator '{condition.operator}'.",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return cast(ColumnElement[bool], expression)
|
|
242
|
+
|
|
243
|
+
def _build_filter_clause(
|
|
244
|
+
self: Self, table: Table, filters: list["FilterCondition"]
|
|
245
|
+
) -> ColumnElement[bool] | None:
|
|
246
|
+
filter_clause: ColumnElement[bool] | None = None
|
|
247
|
+
|
|
248
|
+
for condition in filters:
|
|
249
|
+
expression = self._build_filter_expression(table, condition)
|
|
250
|
+
|
|
251
|
+
if filter_clause is None:
|
|
252
|
+
filter_clause = expression
|
|
253
|
+
elif condition.link == "or":
|
|
254
|
+
filter_clause = cast(ColumnElement[bool], or_(filter_clause, expression))
|
|
255
|
+
else:
|
|
256
|
+
filter_clause = cast(ColumnElement[bool], and_(filter_clause, expression))
|
|
257
|
+
|
|
258
|
+
return filter_clause
|
|
259
|
+
|
|
260
|
+
def _build_exact_match_clause(
|
|
261
|
+
self: Self, table: Table, matches: list["ColumnValueMatch"]
|
|
262
|
+
) -> ColumnElement[bool] | None:
|
|
263
|
+
expressions = [
|
|
264
|
+
self._get_column_or_400(table, match.column) == match.value
|
|
265
|
+
for match in matches
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
if not expressions:
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
return cast(ColumnElement[bool], and_(*expressions))
|
|
272
|
+
|
|
273
|
+
def _build_match_groups_clause(
|
|
274
|
+
self: Self, table: Table, match_groups: list[list["ColumnValueMatch"]]
|
|
275
|
+
) -> ColumnElement[bool] | None:
|
|
276
|
+
group_clauses = [
|
|
277
|
+
group_clause
|
|
278
|
+
for group in match_groups
|
|
279
|
+
if (group_clause := self._build_exact_match_clause(table, group)) is not None
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
if not group_clauses:
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
return cast(ColumnElement[bool], or_(*group_clauses))
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def _combine_with_and(
|
|
289
|
+
clauses: list[ColumnElement[bool]],
|
|
290
|
+
) -> ColumnElement[bool] | None:
|
|
291
|
+
if not clauses:
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
return cast(ColumnElement[bool], and_(*clauses))
|
|
295
|
+
|
|
296
|
+
def get_rows_advanced(
|
|
297
|
+
self: Self, table_name: str, payload: "FilterRequest"
|
|
298
|
+
) -> GetRowsResponse:
|
|
299
|
+
"""
|
|
300
|
+
Advanced filtering using logical operator trees matching custom UI filters.
|
|
301
|
+
"""
|
|
302
|
+
table = self._get_table_or_404(table_name)
|
|
303
|
+
|
|
195
304
|
rows_query = select(table)
|
|
196
305
|
count_query = select(func.count()).select_from(table)
|
|
197
306
|
|
|
198
|
-
|
|
199
|
-
if
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if cond.column not in table.c:
|
|
204
|
-
continue # Guard against non-existent columns safely
|
|
205
|
-
|
|
206
|
-
col = table.c[cond.column]
|
|
207
|
-
val = cond.value
|
|
208
|
-
|
|
209
|
-
# Map string operators to SQLAlchemy expressions
|
|
210
|
-
if cond.operator == "equals":
|
|
211
|
-
expr = col == val
|
|
212
|
-
elif cond.operator == "not_equals":
|
|
213
|
-
expr = col != val
|
|
214
|
-
elif cond.operator == "contains":
|
|
215
|
-
expr = col.ilike(f"%{val}%")
|
|
216
|
-
elif cond.operator == "starts_with":
|
|
217
|
-
expr = col.ilike(f"{val}%")
|
|
218
|
-
elif cond.operator == "ends_with":
|
|
219
|
-
expr = col.ilike(f"%{val}")
|
|
220
|
-
elif cond.operator == "greater_than":
|
|
221
|
-
expr = col > val
|
|
222
|
-
elif cond.operator == "less_than":
|
|
223
|
-
expr = col < val
|
|
224
|
-
elif cond.operator == "greater_than_or_equals":
|
|
225
|
-
expr = col >= val
|
|
226
|
-
elif cond.operator == "less_than_or_equals":
|
|
227
|
-
expr = col <= val
|
|
228
|
-
else:
|
|
229
|
-
continue
|
|
230
|
-
|
|
231
|
-
# Combine them based on the link field ('and' / 'or')
|
|
232
|
-
if not conditions:
|
|
233
|
-
conditions.append(expr)
|
|
234
|
-
else:
|
|
235
|
-
if cond.link == "or":
|
|
236
|
-
# Merge last expression with current one via OR
|
|
237
|
-
prev_expr = conditions.pop()
|
|
238
|
-
conditions.append(or_(prev_expr, expr))
|
|
239
|
-
else:
|
|
240
|
-
# Default to AND chaining
|
|
241
|
-
prev_expr = conditions.pop()
|
|
242
|
-
conditions.append(and_(prev_expr, expr))
|
|
243
|
-
|
|
244
|
-
if conditions:
|
|
245
|
-
rows_query = rows_query.where(*conditions)
|
|
246
|
-
count_query = count_query.where(*conditions)
|
|
247
|
-
|
|
248
|
-
# Apply Pagination
|
|
307
|
+
filter_clause = self._build_filter_clause(table, payload.filters)
|
|
308
|
+
if filter_clause is not None:
|
|
309
|
+
rows_query = rows_query.where(filter_clause)
|
|
310
|
+
count_query = count_query.where(filter_clause)
|
|
311
|
+
|
|
249
312
|
rows_query = rows_query.limit(payload.limit).offset(payload.offset)
|
|
250
313
|
|
|
251
|
-
# Execute
|
|
252
314
|
with self.engine.connect() as connection:
|
|
253
315
|
total = int(connection.execute(count_query).scalar_one())
|
|
254
316
|
result = connection.execute(rows_query)
|
|
255
317
|
rows = [dict(row) for row in result.mappings()]
|
|
256
318
|
|
|
257
319
|
return {"rows": rows, "total_count": total}
|
|
320
|
+
|
|
321
|
+
def delete_rows(
|
|
322
|
+
self: Self, table_name: str, payload: "DeleteRowsRequest"
|
|
323
|
+
) -> DeleteRowsResponse:
|
|
324
|
+
table = self._get_table_or_404(table_name)
|
|
325
|
+
|
|
326
|
+
clauses: list[ColumnElement[bool]] = []
|
|
327
|
+
|
|
328
|
+
match_clause = self._build_exact_match_clause(table, payload.matches)
|
|
329
|
+
if match_clause is not None:
|
|
330
|
+
clauses.append(match_clause)
|
|
331
|
+
|
|
332
|
+
match_groups_clause = self._build_match_groups_clause(table, payload.match_groups)
|
|
333
|
+
if match_groups_clause is not None:
|
|
334
|
+
clauses.append(match_groups_clause)
|
|
335
|
+
|
|
336
|
+
filter_clause = self._build_filter_clause(table, payload.filters)
|
|
337
|
+
if filter_clause is not None:
|
|
338
|
+
clauses.append(filter_clause)
|
|
339
|
+
|
|
340
|
+
where_clause = self._combine_with_and(clauses)
|
|
341
|
+
if where_clause is None:
|
|
342
|
+
raise HTTPException(
|
|
343
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
344
|
+
detail="At least one match or filter is required to delete rows.",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
delete_query = delete(table).where(where_clause)
|
|
348
|
+
|
|
349
|
+
with self.engine.begin() as connection:
|
|
350
|
+
result = connection.execute(delete_query)
|
|
351
|
+
|
|
352
|
+
deleted_count = result.rowcount if result.rowcount is not None else 0
|
|
353
|
+
return {"deleted_count": max(deleted_count, 0)}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from sqlalchemy_studio.Studio import Studio
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, HTTPException, status
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
OperatorType = Literal[
|
|
10
|
+
"equals",
|
|
11
|
+
"not_equals",
|
|
12
|
+
"contains",
|
|
13
|
+
"starts_with",
|
|
14
|
+
"ends_with",
|
|
15
|
+
"greater_than",
|
|
16
|
+
"less_than",
|
|
17
|
+
"greater_than_or_equals",
|
|
18
|
+
"less_than_or_equals",
|
|
19
|
+
]
|
|
20
|
+
LinkType = Literal["and", "or"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FilterCondition(BaseModel):
|
|
24
|
+
column: str
|
|
25
|
+
operator: OperatorType
|
|
26
|
+
value: Any
|
|
27
|
+
link: LinkType = "and"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FilterRequest(BaseModel):
|
|
31
|
+
limit: int = Field(default=10, ge=1)
|
|
32
|
+
offset: int = Field(default=0, ge=0)
|
|
33
|
+
filters: list[FilterCondition] = Field(default_factory=list)
|
|
34
|
+
# optional projection of columns to return
|
|
35
|
+
columns: list[str] = Field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SortDirection(str):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SortSpec(BaseModel):
|
|
43
|
+
column: str
|
|
44
|
+
direction: str = Field(default="asc")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ColumnValueMatch(BaseModel):
|
|
48
|
+
column: str
|
|
49
|
+
value: Any
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DeleteRowsRequest(BaseModel):
|
|
53
|
+
# matches for a single-row delete (AND across entries), kept for backward compat
|
|
54
|
+
matches: list[ColumnValueMatch] = Field(default_factory=list)
|
|
55
|
+
# groups of matches to delete multiple specific rows. Each group is ANDed,
|
|
56
|
+
# groups are ORed together.
|
|
57
|
+
match_groups: list[list[ColumnValueMatch]] = Field(default_factory=list)
|
|
58
|
+
filters: list[FilterCondition] = Field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class DeleteRowsResponse(BaseModel):
|
|
62
|
+
deleted_count: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_tables_router(studio: "Studio") -> APIRouter:
|
|
66
|
+
router = APIRouter()
|
|
67
|
+
|
|
68
|
+
@router.get("/api/tables")
|
|
69
|
+
def get_tables() -> Any:
|
|
70
|
+
return studio.show_tables()
|
|
71
|
+
|
|
72
|
+
@router.get("/api/tables/{name}")
|
|
73
|
+
def get_table(name: str) -> Any:
|
|
74
|
+
return studio.show_table(name)
|
|
75
|
+
|
|
76
|
+
@router.post("/api/{table_name}/query")
|
|
77
|
+
async def query_table_rows(table_name: str, payload: FilterRequest) -> Any:
|
|
78
|
+
"""
|
|
79
|
+
Exposes advanced database table operations to handle compound UI filters.
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
return studio.get_rows_advanced(table_name, payload)
|
|
83
|
+
except HTTPException as e:
|
|
84
|
+
raise e
|
|
85
|
+
except Exception as e:
|
|
86
|
+
raise HTTPException(
|
|
87
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
88
|
+
detail=f"Query builder failed: {str(e)}",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@router.post("/api/{table_name}/delete", response_model=DeleteRowsResponse)
|
|
92
|
+
async def delete_table_rows(
|
|
93
|
+
table_name: str, payload: DeleteRowsRequest
|
|
94
|
+
) -> DeleteRowsResponse:
|
|
95
|
+
try:
|
|
96
|
+
result = studio.delete_rows(table_name, payload)
|
|
97
|
+
return DeleteRowsResponse(**result)
|
|
98
|
+
except HTTPException as e:
|
|
99
|
+
raise e
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise HTTPException(
|
|
102
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
103
|
+
detail=f"Delete rows failed: {str(e)}",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return router
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:.375rem;--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.static{position:static}.order-last{order:9999}.col-span-2{grid-column:span 2/span 2}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.flex{display:flex}.grid{display:grid}.hidden{display:none}.table{display:table}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.max-h-\[45vh\]{max-height:45vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.min-h-screen{min-height:100vh}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-20{width:calc(var(--spacing) * 20)}.w-full{width:100%}.w-max{width:max-content}.max-w-12{max-width:calc(var(--spacing) * 12)}.max-w-48{max-width:calc(var(--spacing) * 48)}.max-w-72{max-width:calc(var(--spacing) * 72)}.max-w-full{max-width:100%}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-12{min-width:calc(var(--spacing) * 12)}.min-w-full{min-width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.animate-spin{animation:var(--animate-spin)}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-\[2\.25rem_minmax\(0\,1fr\)\]{grid-template-columns:2.25rem minmax(0,1fr)}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.rounded{border-radius:.25rem}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-neutral-600{border-color:var(--color-neutral-600)}.border-neutral-800{border-color:var(--color-neutral-800)}.border-red-900\/60{border-color:#82181a99}@supports (color:color-mix(in lab, red, red)){.border-red-900\/60{border-color:color-mix(in oklab, var(--color-red-900) 60%, transparent)}}.bg-neutral-700{background-color:var(--color-neutral-700)}.bg-neutral-800{background-color:var(--color-neutral-800)}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-neutral-900\/40{background-color:#17171766}@supports (color:color-mix(in lab, red, red)){.bg-neutral-900\/40{background-color:color-mix(in oklab, var(--color-neutral-900) 40%, transparent)}}.bg-neutral-950{background-color:var(--color-neutral-950)}.bg-red-700{background-color:var(--color-red-700)}.bg-red-950\/40{background-color:#46080966}@supports (color:color-mix(in lab, red, red)){.bg-red-950\/40{background-color:color-mix(in oklab, var(--color-red-950) 40%, transparent)}}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-8{padding:calc(var(--spacing) * 8)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-6{padding-block:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.whitespace-nowrap{white-space:nowrap}.text-neutral-100{color:var(--color-neutral-100)}.text-neutral-200{color:var(--color-neutral-200)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-red-300{color:var(--color-red-300)}.text-red-400{color:var(--color-red-400)}.text-white{color:var(--color-white)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.placeholder\:text-neutral-500::placeholder{color:var(--color-neutral-500)}@media (hover:hover){.hover\:border-neutral-700:hover{border-color:var(--color-neutral-700)}.hover\:bg-neutral-700:hover{background-color:var(--color-neutral-700)}.hover\:bg-neutral-800:hover{background-color:var(--color-neutral-800)}.hover\:bg-neutral-900:hover{background-color:var(--color-neutral-900)}.hover\:bg-red-600:hover{background-color:var(--color-red-600)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:text-neutral-100:hover{color:var(--color-neutral-100)}}.focus\:border-neutral-600:focus{border-color:var(--color-neutral-600)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-35:disabled{opacity:.35}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:order-none{order:0}.sm\:ml-0{margin-left:calc(var(--spacing) * 0)}.sm\:w-auto{width:auto}.sm\:min-w-32{min-width:calc(var(--spacing) * 32)}.sm\:flex-none{flex:none}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:justify-end{justify-content:flex-end}.sm\:p-4{padding:calc(var(--spacing) * 4)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}}@media (width>=48rem){.md\:col-span-1{grid-column:span 1/span 1}.md\:grid-cols-\[2\.25rem_minmax\(5rem\,6rem\)_minmax\(9rem\,14rem\)_minmax\(10rem\,16rem\)_minmax\(10rem\,1fr\)\]{grid-template-columns:2.25rem minmax(5rem,6rem) minmax(9rem,14rem) minmax(10rem,16rem) minmax(10rem,1fr)}}@media (width>=64rem){.lg\:mb-4{margin-bottom:calc(var(--spacing) * 4)}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:h-screen{height:100vh}.lg\:max-h-none{max-height:none}.lg\:w-72{width:calc(var(--spacing) * 72)}.lg\:flex-row{flex-direction:row}.lg\:overflow-auto{overflow:auto}.lg\:overflow-hidden{overflow:hidden}.lg\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.lg\:border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.lg\:p-6{padding:calc(var(--spacing) * 6)}}}*{box-sizing:border-box}html,body,#app{min-height:100%}body{margin:0}.app-root{min-height:100vh}.app-root ::-webkit-scrollbar{width:8px;height:8px}.app-root ::-webkit-scrollbar-thumb{background:#7373732e;border-radius:8px}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
|