sqlalchemy-studio 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.
- sqlalchemy_studio-0.1.0/PKG-INFO +103 -0
- sqlalchemy_studio-0.1.0/README.md +80 -0
- sqlalchemy_studio-0.1.0/pyproject.toml +27 -0
- sqlalchemy_studio-0.1.0/setup.cfg +4 -0
- sqlalchemy_studio-0.1.0/sqlalchemy_studio.egg-info/PKG-INFO +103 -0
- sqlalchemy_studio-0.1.0/sqlalchemy_studio.egg-info/SOURCES.txt +10 -0
- sqlalchemy_studio-0.1.0/sqlalchemy_studio.egg-info/dependency_links.txt +1 -0
- sqlalchemy_studio-0.1.0/sqlalchemy_studio.egg-info/requires.txt +5 -0
- sqlalchemy_studio-0.1.0/sqlalchemy_studio.egg-info/top_level.txt +1 -0
- sqlalchemy_studio-0.1.0/studio/Studio.py +232 -0
- sqlalchemy_studio-0.1.0/studio/__init__.py +1 -0
- sqlalchemy_studio-0.1.0/studio/backend.py +68 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-studio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: Xursand Qarlibayev <coderxuz2009@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/coderxuz/sqlalchemy-studio
|
|
8
|
+
Project-URL: Source, https://github.com/coderxuz/sqlalchemy-studio
|
|
9
|
+
Keywords: sqlalchemy,database,inspector,studio
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: build>=1.5.0
|
|
19
|
+
Requires-Dist: fastapi>=0.136.3
|
|
20
|
+
Requires-Dist: sqlalchemy>=2.0.50
|
|
21
|
+
Requires-Dist: twine>=6.2.0
|
|
22
|
+
Requires-Dist: uvicorn>=0.48.0
|
|
23
|
+
|
|
24
|
+
# sqlalchemy-studio-backend
|
|
25
|
+
|
|
26
|
+
Backend utilities for inspecting and serving database tables via a FastAPI `Studio`.
|
|
27
|
+
|
|
28
|
+
This package exposes a small FastAPI app that can be embedded in your application or run
|
|
29
|
+
standalone to inspect a SQLAlchemy-accessible database.
|
|
30
|
+
|
|
31
|
+
Quick summary
|
|
32
|
+
- API endpoints (when using the packaged server):
|
|
33
|
+
- `GET /api/tables` — list tables and columns
|
|
34
|
+
- `GET /api/tables/{name}` — describe a single table
|
|
35
|
+
- `POST /api/{table_name}/query` — run the UI's advanced query payload
|
|
36
|
+
|
|
37
|
+
Install
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
pip install sqlalchemy-studio-backend
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or install from the repository for development/testing:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
pip install --upgrade git+https://github.com/yourusername/sqlalchemy-studio.git@main#subdirectory=sqlalchemy-studio-backend
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Quickstart
|
|
50
|
+
|
|
51
|
+
```py
|
|
52
|
+
from sqlalchemy import create_engine
|
|
53
|
+
from sqlalchemy.orm import declarative_base
|
|
54
|
+
from db.main import Studio
|
|
55
|
+
|
|
56
|
+
Base = declarative_base()
|
|
57
|
+
engine = create_engine("sqlite:///test.db")
|
|
58
|
+
|
|
59
|
+
studio = Studio(engine, Base)
|
|
60
|
+
# Starts uvicorn and serves API under /api
|
|
61
|
+
studio.run(port=9000)
|
|
62
|
+
# API available at http://localhost:9000/api
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Serving a built frontend (optional)
|
|
66
|
+
|
|
67
|
+
If you build the Vite frontend, copy its `dist` output into the backend `static` folder
|
|
68
|
+
and the backend will serve the SPA from `/` while keeping the API under `/api`.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# from repository root
|
|
74
|
+
npm --prefix sqlalchemy-studio-front install
|
|
75
|
+
npm --prefix sqlalchemy-studio-front run build
|
|
76
|
+
rm -rf sqlalchemy-studio-backend/static
|
|
77
|
+
mkdir -p sqlalchemy-studio-backend/static
|
|
78
|
+
cp -r sqlalchemy-studio-front/dist/* sqlalchemy-studio-backend/static/
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Configuration notes
|
|
82
|
+
- By default the package mounts static files at `/` and prefixes API routes with `/api` to
|
|
83
|
+
avoid SPA route collisions. Adjust `db/main.py` if you need a different layout.
|
|
84
|
+
- CORS: `Studio` registers a small set of development origins. When serving the SPA
|
|
85
|
+
from the same server you won't need CORS; keep/update `Studio._set_cors` for other setups.
|
|
86
|
+
|
|
87
|
+
Publishing
|
|
88
|
+
|
|
89
|
+
1. Build the distribution locally:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python -m pip install --upgrade build twine
|
|
93
|
+
python -m build
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
2. Upload to PyPI (CI should set `PYPI_API_TOKEN`):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m twine upload dist/*
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
License
|
|
103
|
+
MIT — update as appropriate.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# sqlalchemy-studio-backend
|
|
2
|
+
|
|
3
|
+
Backend utilities for inspecting and serving database tables via a FastAPI `Studio`.
|
|
4
|
+
|
|
5
|
+
This package exposes a small FastAPI app that can be embedded in your application or run
|
|
6
|
+
standalone to inspect a SQLAlchemy-accessible database.
|
|
7
|
+
|
|
8
|
+
Quick summary
|
|
9
|
+
- API endpoints (when using the packaged server):
|
|
10
|
+
- `GET /api/tables` — list tables and columns
|
|
11
|
+
- `GET /api/tables/{name}` — describe a single table
|
|
12
|
+
- `POST /api/{table_name}/query` — run the UI's advanced query payload
|
|
13
|
+
|
|
14
|
+
Install
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pip install sqlalchemy-studio-backend
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install from the repository for development/testing:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
pip install --upgrade git+https://github.com/yourusername/sqlalchemy-studio.git@main#subdirectory=sqlalchemy-studio-backend
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Quickstart
|
|
27
|
+
|
|
28
|
+
```py
|
|
29
|
+
from sqlalchemy import create_engine
|
|
30
|
+
from sqlalchemy.orm import declarative_base
|
|
31
|
+
from db.main import Studio
|
|
32
|
+
|
|
33
|
+
Base = declarative_base()
|
|
34
|
+
engine = create_engine("sqlite:///test.db")
|
|
35
|
+
|
|
36
|
+
studio = Studio(engine, Base)
|
|
37
|
+
# Starts uvicorn and serves API under /api
|
|
38
|
+
studio.run(port=9000)
|
|
39
|
+
# API available at http://localhost:9000/api
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Serving a built frontend (optional)
|
|
43
|
+
|
|
44
|
+
If you build the Vite frontend, copy its `dist` output into the backend `static` folder
|
|
45
|
+
and the backend will serve the SPA from `/` while keeping the API under `/api`.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# from repository root
|
|
51
|
+
npm --prefix sqlalchemy-studio-front install
|
|
52
|
+
npm --prefix sqlalchemy-studio-front run build
|
|
53
|
+
rm -rf sqlalchemy-studio-backend/static
|
|
54
|
+
mkdir -p sqlalchemy-studio-backend/static
|
|
55
|
+
cp -r sqlalchemy-studio-front/dist/* sqlalchemy-studio-backend/static/
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Configuration notes
|
|
59
|
+
- By default the package mounts static files at `/` and prefixes API routes with `/api` to
|
|
60
|
+
avoid SPA route collisions. Adjust `db/main.py` if you need a different layout.
|
|
61
|
+
- CORS: `Studio` registers a small set of development origins. When serving the SPA
|
|
62
|
+
from the same server you won't need CORS; keep/update `Studio._set_cors` for other setups.
|
|
63
|
+
|
|
64
|
+
Publishing
|
|
65
|
+
|
|
66
|
+
1. Build the distribution locally:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python -m pip install --upgrade build twine
|
|
70
|
+
python -m build
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
2. Upload to PyPI (CI should set `PYPI_API_TOKEN`):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python -m twine upload dist/*
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
License
|
|
80
|
+
MIT — update as appropriate.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sqlalchemy-studio"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"build>=1.5.0",
|
|
9
|
+
"fastapi>=0.136.3",
|
|
10
|
+
"sqlalchemy>=2.0.50",
|
|
11
|
+
"twine>=6.2.0",
|
|
12
|
+
"uvicorn>=0.48.0",
|
|
13
|
+
]
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Xursand Qarlibayev", email = "coderxuz2009@gmail.com" },
|
|
16
|
+
]
|
|
17
|
+
license = { text = "MIT" }
|
|
18
|
+
keywords = ["sqlalchemy", "database", "inspector", "studio"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
]
|
|
27
|
+
urls = { "Homepage" = "https://github.com/coderxuz/sqlalchemy-studio", "Source" = "https://github.com/coderxuz/sqlalchemy-studio" }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlalchemy-studio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: Xursand Qarlibayev <coderxuz2009@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/coderxuz/sqlalchemy-studio
|
|
8
|
+
Project-URL: Source, https://github.com/coderxuz/sqlalchemy-studio
|
|
9
|
+
Keywords: sqlalchemy,database,inspector,studio
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: build>=1.5.0
|
|
19
|
+
Requires-Dist: fastapi>=0.136.3
|
|
20
|
+
Requires-Dist: sqlalchemy>=2.0.50
|
|
21
|
+
Requires-Dist: twine>=6.2.0
|
|
22
|
+
Requires-Dist: uvicorn>=0.48.0
|
|
23
|
+
|
|
24
|
+
# sqlalchemy-studio-backend
|
|
25
|
+
|
|
26
|
+
Backend utilities for inspecting and serving database tables via a FastAPI `Studio`.
|
|
27
|
+
|
|
28
|
+
This package exposes a small FastAPI app that can be embedded in your application or run
|
|
29
|
+
standalone to inspect a SQLAlchemy-accessible database.
|
|
30
|
+
|
|
31
|
+
Quick summary
|
|
32
|
+
- API endpoints (when using the packaged server):
|
|
33
|
+
- `GET /api/tables` — list tables and columns
|
|
34
|
+
- `GET /api/tables/{name}` — describe a single table
|
|
35
|
+
- `POST /api/{table_name}/query` — run the UI's advanced query payload
|
|
36
|
+
|
|
37
|
+
Install
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
pip install sqlalchemy-studio-backend
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or install from the repository for development/testing:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
pip install --upgrade git+https://github.com/yourusername/sqlalchemy-studio.git@main#subdirectory=sqlalchemy-studio-backend
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Quickstart
|
|
50
|
+
|
|
51
|
+
```py
|
|
52
|
+
from sqlalchemy import create_engine
|
|
53
|
+
from sqlalchemy.orm import declarative_base
|
|
54
|
+
from db.main import Studio
|
|
55
|
+
|
|
56
|
+
Base = declarative_base()
|
|
57
|
+
engine = create_engine("sqlite:///test.db")
|
|
58
|
+
|
|
59
|
+
studio = Studio(engine, Base)
|
|
60
|
+
# Starts uvicorn and serves API under /api
|
|
61
|
+
studio.run(port=9000)
|
|
62
|
+
# API available at http://localhost:9000/api
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Serving a built frontend (optional)
|
|
66
|
+
|
|
67
|
+
If you build the Vite frontend, copy its `dist` output into the backend `static` folder
|
|
68
|
+
and the backend will serve the SPA from `/` while keeping the API under `/api`.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# from repository root
|
|
74
|
+
npm --prefix sqlalchemy-studio-front install
|
|
75
|
+
npm --prefix sqlalchemy-studio-front run build
|
|
76
|
+
rm -rf sqlalchemy-studio-backend/static
|
|
77
|
+
mkdir -p sqlalchemy-studio-backend/static
|
|
78
|
+
cp -r sqlalchemy-studio-front/dist/* sqlalchemy-studio-backend/static/
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Configuration notes
|
|
82
|
+
- By default the package mounts static files at `/` and prefixes API routes with `/api` to
|
|
83
|
+
avoid SPA route collisions. Adjust `db/main.py` if you need a different layout.
|
|
84
|
+
- CORS: `Studio` registers a small set of development origins. When serving the SPA
|
|
85
|
+
from the same server you won't need CORS; keep/update `Studio._set_cors` for other setups.
|
|
86
|
+
|
|
87
|
+
Publishing
|
|
88
|
+
|
|
89
|
+
1. Build the distribution locally:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python -m pip install --upgrade build twine
|
|
93
|
+
python -m build
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
2. Upload to PyPI (CI should set `PYPI_API_TOKEN`):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m twine upload dist/*
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
License
|
|
103
|
+
MIT — update as appropriate.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
sqlalchemy_studio.egg-info/PKG-INFO
|
|
4
|
+
sqlalchemy_studio.egg-info/SOURCES.txt
|
|
5
|
+
sqlalchemy_studio.egg-info/dependency_links.txt
|
|
6
|
+
sqlalchemy_studio.egg-info/requires.txt
|
|
7
|
+
sqlalchemy_studio.egg-info/top_level.txt
|
|
8
|
+
studio/Studio.py
|
|
9
|
+
studio/__init__.py
|
|
10
|
+
studio/backend.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
studio
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from sqlalchemy import and_, create_engine, func, inspect, Engine, or_, select, Table
|
|
2
|
+
from sqlalchemy.engine.interfaces import ReflectedColumn
|
|
3
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
4
|
+
from fastapi import FastAPI, HTTPException, status
|
|
5
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
6
|
+
from fastapi.staticfiles import StaticFiles
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from studio .backend import create_tables_router
|
|
11
|
+
|
|
12
|
+
from typing import Self, TypedDict, Any, cast, TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from studio.backend import FilterRequest
|
|
16
|
+
|
|
17
|
+
engine = create_engine("sqlite:///test.db")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TablesType(TypedDict):
|
|
21
|
+
table: str
|
|
22
|
+
columns: list[dict]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ColumnDetails(TypedDict):
|
|
26
|
+
name: str
|
|
27
|
+
type: str
|
|
28
|
+
nullable: bool
|
|
29
|
+
default: Any
|
|
30
|
+
primary_key: bool
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SingleTableType(TypedDict):
|
|
34
|
+
table: str
|
|
35
|
+
primary_keys: list[str]
|
|
36
|
+
columns: list[ColumnDetails]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GetRowsResponse(TypedDict):
|
|
40
|
+
rows: list[dict[str, Any]]
|
|
41
|
+
total_count: int
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Studio:
|
|
45
|
+
def __init__(self: Self, engine: Engine, base: DeclarativeBase) -> None:
|
|
46
|
+
self.engine = engine
|
|
47
|
+
self.inspector = inspect(self.engine)
|
|
48
|
+
self.app = FastAPI()
|
|
49
|
+
self._register_routes()
|
|
50
|
+
# Serve built frontend from the `static` directory at the project root.
|
|
51
|
+
# Keep this catch-all mount after API routes so `/api/*` resolves first.
|
|
52
|
+
|
|
53
|
+
self._set_cors()
|
|
54
|
+
self.base = base
|
|
55
|
+
|
|
56
|
+
def _set_cors(self):
|
|
57
|
+
origins = [
|
|
58
|
+
"http://localhost:5500",
|
|
59
|
+
"http://localhost:5173",
|
|
60
|
+
"http://localhost:8000",
|
|
61
|
+
"http://localhost:3000",
|
|
62
|
+
]
|
|
63
|
+
self.app.add_middleware(
|
|
64
|
+
CORSMiddleware,
|
|
65
|
+
allow_origins=origins,
|
|
66
|
+
allow_credentials=True,
|
|
67
|
+
allow_methods=["*"],
|
|
68
|
+
allow_headers=["*"],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _register_routes(self):
|
|
72
|
+
self.app.include_router(create_tables_router(self))
|
|
73
|
+
self.app.mount("/", StaticFiles(directory="./studio/static", html=True), name="static")
|
|
74
|
+
|
|
75
|
+
def run(self, port: int = 8000):
|
|
76
|
+
print("-"*50)
|
|
77
|
+
print("-"*50)
|
|
78
|
+
print(f"link:http://localhost:{port}")
|
|
79
|
+
print("-"*50)
|
|
80
|
+
print("-"*50)
|
|
81
|
+
uvicorn.run(self.app, host="0.0.0.0", port=port)
|
|
82
|
+
|
|
83
|
+
def show_tables(self: Self) -> list[TablesType]:
|
|
84
|
+
|
|
85
|
+
tables_list: list[TablesType] = []
|
|
86
|
+
tables = self.inspector.get_table_names()
|
|
87
|
+
for table in tables:
|
|
88
|
+
tables_list.append(
|
|
89
|
+
{
|
|
90
|
+
"table": table,
|
|
91
|
+
"columns": [
|
|
92
|
+
{
|
|
93
|
+
**c,
|
|
94
|
+
"type": str(c["type"]),
|
|
95
|
+
}
|
|
96
|
+
for c in self.inspector.get_columns(table)
|
|
97
|
+
],
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
return tables_list
|
|
101
|
+
|
|
102
|
+
def show_table(self: Self, table_name: str) -> SingleTableType:
|
|
103
|
+
"""
|
|
104
|
+
Returns the structural data (columns, types, primary keys)
|
|
105
|
+
for a single specified table with strict typing for MyPy.
|
|
106
|
+
"""
|
|
107
|
+
# 1. Check if the table exists
|
|
108
|
+
if not self.inspector.has_table(table_name):
|
|
109
|
+
raise HTTPException(
|
|
110
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
111
|
+
detail=f"Table '{table_name}' not found.",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# 2. Reflect the table (keeps metadata clean/extended)
|
|
115
|
+
Table(
|
|
116
|
+
table_name,
|
|
117
|
+
self.base.metadata,
|
|
118
|
+
autoload_with=self.engine,
|
|
119
|
+
extend_existing=True,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# 3. Extract primary key details safely
|
|
123
|
+
pk_constraint = self.inspector.get_pk_constraint(table_name) or {}
|
|
124
|
+
# MyPy needs to know this evaluates cleanly to a list of strings
|
|
125
|
+
primary_keys = cast(list[str], pk_constraint.get("constrained_columns", []))
|
|
126
|
+
# 4. Extract and type-cast column information
|
|
127
|
+
columns_data: list[ColumnDetails] = []
|
|
128
|
+
|
|
129
|
+
# self.inspector.get_columns returns a list of dictionaries
|
|
130
|
+
raw_columns = self.inspector.get_columns(table_name)
|
|
131
|
+
|
|
132
|
+
for col in raw_columns:
|
|
133
|
+
columns_data.append(
|
|
134
|
+
{
|
|
135
|
+
"name": cast(str, col["name"]),
|
|
136
|
+
"type": str(col["type"]), # Stringify the type object for JSON
|
|
137
|
+
"nullable": bool(col["nullable"]),
|
|
138
|
+
"default": col.get("default"),
|
|
139
|
+
"primary_key": col.get("primary_key", 0) > 0,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# 5. Construct the final strictly typed response
|
|
144
|
+
response_data: SingleTableType = {
|
|
145
|
+
"table": table_name,
|
|
146
|
+
"primary_keys": primary_keys,
|
|
147
|
+
"columns": columns_data,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return response_data
|
|
151
|
+
|
|
152
|
+
def get_rows_advanced(
|
|
153
|
+
self: Self, table_name: str, payload: "FilterRequest"
|
|
154
|
+
) -> GetRowsResponse:
|
|
155
|
+
"""
|
|
156
|
+
Advanced filtering using logical operator trees matching custom UI filters.
|
|
157
|
+
"""
|
|
158
|
+
if not self.inspector.has_table(table_name):
|
|
159
|
+
raise HTTPException(
|
|
160
|
+
status_code=404, detail=f"Table '{table_name}' not found."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
table = Table(
|
|
164
|
+
table_name,
|
|
165
|
+
self.base.metadata,
|
|
166
|
+
autoload_with=self.engine,
|
|
167
|
+
extend_existing=True,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
rows_query = select(table)
|
|
171
|
+
count_query = select(func.count()).select_from(table)
|
|
172
|
+
|
|
173
|
+
# Build dynamic expressions clauses
|
|
174
|
+
if payload.filters:
|
|
175
|
+
conditions = []
|
|
176
|
+
|
|
177
|
+
for cond in payload.filters:
|
|
178
|
+
if cond.column not in table.c:
|
|
179
|
+
continue # Guard against non-existent columns safely
|
|
180
|
+
|
|
181
|
+
col = table.c[cond.column]
|
|
182
|
+
val = cond.value
|
|
183
|
+
|
|
184
|
+
# Map string operators to SQLAlchemy expressions
|
|
185
|
+
if cond.operator == "equals":
|
|
186
|
+
expr = col == val
|
|
187
|
+
elif cond.operator == "not_equals":
|
|
188
|
+
expr = col != val
|
|
189
|
+
elif cond.operator == "contains":
|
|
190
|
+
expr = col.ilike(f"%{val}%")
|
|
191
|
+
elif cond.operator == "starts_with":
|
|
192
|
+
expr = col.ilike(f"{val}%")
|
|
193
|
+
elif cond.operator == "ends_with":
|
|
194
|
+
expr = col.ilike(f"%{val}")
|
|
195
|
+
elif cond.operator == "greater_than":
|
|
196
|
+
expr = col > val
|
|
197
|
+
elif cond.operator == "less_than":
|
|
198
|
+
expr = col < val
|
|
199
|
+
elif cond.operator == "greater_than_or_equals":
|
|
200
|
+
expr = col >= val
|
|
201
|
+
elif cond.operator == "less_than_or_equals":
|
|
202
|
+
expr = col <= val
|
|
203
|
+
else:
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
# Combine them based on the link field ('and' / 'or')
|
|
207
|
+
if not conditions:
|
|
208
|
+
conditions.append(expr)
|
|
209
|
+
else:
|
|
210
|
+
if cond.link == "or":
|
|
211
|
+
# Merge last expression with current one via OR
|
|
212
|
+
prev_expr = conditions.pop()
|
|
213
|
+
conditions.append(or_(prev_expr, expr))
|
|
214
|
+
else:
|
|
215
|
+
# Default to AND chaining
|
|
216
|
+
prev_expr = conditions.pop()
|
|
217
|
+
conditions.append(and_(prev_expr, expr))
|
|
218
|
+
|
|
219
|
+
if conditions:
|
|
220
|
+
rows_query = rows_query.where(*conditions)
|
|
221
|
+
count_query = count_query.where(*conditions)
|
|
222
|
+
|
|
223
|
+
# Apply Pagination
|
|
224
|
+
rows_query = rows_query.limit(payload.limit).offset(payload.offset)
|
|
225
|
+
|
|
226
|
+
# Execute
|
|
227
|
+
with self.engine.connect() as connection:
|
|
228
|
+
total = int(connection.execute(count_query).scalar_one())
|
|
229
|
+
result = connection.execute(rows_query)
|
|
230
|
+
rows = [dict(row) for row in result.mappings()]
|
|
231
|
+
|
|
232
|
+
return {"rows": rows, "total_count": total}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from studio.Studio import Studio
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from fastapi import APIRouter, Query, HTTPException,status
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from backend.Studio import Studio
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal, Self, TypedDict
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
# Define acceptable operators matching your UI
|
|
12
|
+
OperatorType = Literal[
|
|
13
|
+
"equals",
|
|
14
|
+
"not_equals",
|
|
15
|
+
"contains",
|
|
16
|
+
"starts_with",
|
|
17
|
+
"ends_with",
|
|
18
|
+
"greater_than",
|
|
19
|
+
"less_than",
|
|
20
|
+
"greater_than_or_equals",
|
|
21
|
+
"less_than_or_equals",
|
|
22
|
+
]
|
|
23
|
+
LinkType = Literal["and", "or"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FilterCondition(BaseModel):
|
|
27
|
+
column: str
|
|
28
|
+
operator: OperatorType
|
|
29
|
+
value: Any
|
|
30
|
+
link: LinkType = "and" # Connects this condition to the previous one
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FilterRequest(BaseModel):
|
|
34
|
+
limit: int = Field(default=10, ge=1)
|
|
35
|
+
offset: int = Field(default=0, ge=0)
|
|
36
|
+
filters: list[FilterCondition] = Field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_tables_router(studio: "Studio"):
|
|
40
|
+
router = APIRouter()
|
|
41
|
+
|
|
42
|
+
@router.get("/api/tables")
|
|
43
|
+
def get_tables():
|
|
44
|
+
return studio.show_tables()
|
|
45
|
+
|
|
46
|
+
@router.get("/api/tables/{name}")
|
|
47
|
+
def get_tables(name: str):
|
|
48
|
+
return studio.show_table(name)
|
|
49
|
+
|
|
50
|
+
@router.post("/api/{table_name}/query")
|
|
51
|
+
async def query_table_rows(
|
|
52
|
+
table_name: str,
|
|
53
|
+
payload: FilterRequest
|
|
54
|
+
) -> Any:
|
|
55
|
+
"""
|
|
56
|
+
Exposes advanced database table operations to handle compound UI filters.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
# Assuming `studio` is accessible in your router scope
|
|
60
|
+
return studio.get_rows_advanced(table_name, payload)
|
|
61
|
+
except HTTPException as e:
|
|
62
|
+
raise e
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise HTTPException(
|
|
65
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
66
|
+
detail=f"Query builder failed: {str(e)}"
|
|
67
|
+
)
|
|
68
|
+
return router
|