grimoireplot 0.0.1__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.
- grimoireplot/__init__.py +5 -0
- grimoireplot/client.py +166 -0
- grimoireplot/common.py +32 -0
- grimoireplot/create_some_plots.py +352 -0
- grimoireplot/main.py +284 -0
- grimoireplot/models.py +210 -0
- grimoireplot/server.py +90 -0
- grimoireplot/ui.py +200 -0
- grimoireplot/ui_elements.py +772 -0
- grimoireplot-0.0.1.dist-info/METADATA +217 -0
- grimoireplot-0.0.1.dist-info/RECORD +13 -0
- grimoireplot-0.0.1.dist-info/WHEEL +4 -0
- grimoireplot-0.0.1.dist-info/entry_points.txt +3 -0
grimoireplot/main.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright © 2026 Idiap Research Institute <contact@idiap.ch>
|
|
2
|
+
# SPDX-FileContributor: William Droz <william.droz@idiap.ch>
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Main CLI for GrimoirePlot application.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
from grimoireplot.common import get_grimoire_secret, get_grimoire_server
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def serve_command(args):
|
|
14
|
+
"""Run the GrimoirePlot server."""
|
|
15
|
+
from grimoireplot.server import my_app
|
|
16
|
+
|
|
17
|
+
my_app(host=args.host, port=args.port)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def live_test_command(args):
|
|
21
|
+
"""Run a live test that adds one datapoint every 0.2 seconds to two plots."""
|
|
22
|
+
import time
|
|
23
|
+
import math
|
|
24
|
+
import plotly.graph_objects as go
|
|
25
|
+
from grimoireplot.client import push_plot_sync
|
|
26
|
+
|
|
27
|
+
server_url = f"http://{args.host}:{args.port}"
|
|
28
|
+
grimoire_name = args.grimoire_name
|
|
29
|
+
chapter_name = "Live Test"
|
|
30
|
+
|
|
31
|
+
print(f"Starting live test to {server_url}")
|
|
32
|
+
print(f"Grimoire: {grimoire_name}")
|
|
33
|
+
print(f"Adding one datapoint every {args.interval} seconds to 2 plots")
|
|
34
|
+
print("Press Ctrl+C to stop")
|
|
35
|
+
print("-" * 40)
|
|
36
|
+
|
|
37
|
+
# Data for plot 1 (sine wave)
|
|
38
|
+
x_data_1 = []
|
|
39
|
+
y_data_1 = []
|
|
40
|
+
# Data for plot 2 (cosine wave)
|
|
41
|
+
x_data_2 = []
|
|
42
|
+
y_data_2 = []
|
|
43
|
+
step = 0
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
while args.max_points == 0 or step < args.max_points:
|
|
47
|
+
# Generate new datapoints
|
|
48
|
+
x_data_1.append(step)
|
|
49
|
+
y_data_1.append(math.sin(step * 0.1) + (step * 0.01))
|
|
50
|
+
x_data_2.append(step)
|
|
51
|
+
y_data_2.append(math.cos(step * 0.1) + (step * 0.01))
|
|
52
|
+
|
|
53
|
+
# Create figure 1 (sine wave)
|
|
54
|
+
fig1 = go.Figure()
|
|
55
|
+
fig1.add_trace(
|
|
56
|
+
go.Scatter(
|
|
57
|
+
x=x_data_1,
|
|
58
|
+
y=y_data_1,
|
|
59
|
+
mode="lines+markers",
|
|
60
|
+
name="Sine Wave",
|
|
61
|
+
line=dict(color="blue"),
|
|
62
|
+
marker=dict(size=6),
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
fig1.update_layout(
|
|
66
|
+
title=f"Sine Wave (Point {step + 1})",
|
|
67
|
+
xaxis_title="Step",
|
|
68
|
+
yaxis_title="Value",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Create figure 2 (cosine wave)
|
|
72
|
+
fig2 = go.Figure()
|
|
73
|
+
fig2.add_trace(
|
|
74
|
+
go.Scatter(
|
|
75
|
+
x=x_data_2,
|
|
76
|
+
y=y_data_2,
|
|
77
|
+
mode="lines+markers",
|
|
78
|
+
name="Cosine Wave",
|
|
79
|
+
line=dict(color="red"),
|
|
80
|
+
marker=dict(size=6),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
fig2.update_layout(
|
|
84
|
+
title=f"Cosine Wave (Point {step + 1})",
|
|
85
|
+
xaxis_title="Step",
|
|
86
|
+
yaxis_title="Value",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Push both plots to server
|
|
90
|
+
try:
|
|
91
|
+
push_plot_sync(
|
|
92
|
+
grimoire_name=grimoire_name,
|
|
93
|
+
chapter_name=chapter_name,
|
|
94
|
+
plot_name="Sine Wave",
|
|
95
|
+
fig=fig1,
|
|
96
|
+
grimoire_secret=args.secret,
|
|
97
|
+
grimoire_server=server_url,
|
|
98
|
+
)
|
|
99
|
+
push_plot_sync(
|
|
100
|
+
grimoire_name=grimoire_name,
|
|
101
|
+
chapter_name=chapter_name,
|
|
102
|
+
plot_name="Cosine Wave",
|
|
103
|
+
fig=fig2,
|
|
104
|
+
grimoire_secret=args.secret,
|
|
105
|
+
grimoire_server=server_url,
|
|
106
|
+
)
|
|
107
|
+
print(
|
|
108
|
+
f" Point {step + 1}: sin={y_data_1[-1]:.4f}, cos={y_data_2[-1]:.4f}"
|
|
109
|
+
)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
print(f" [ERROR] Failed to push point {step + 1}: {e}")
|
|
112
|
+
|
|
113
|
+
step += 1
|
|
114
|
+
time.sleep(args.interval)
|
|
115
|
+
|
|
116
|
+
except KeyboardInterrupt:
|
|
117
|
+
print("\n" + "-" * 40)
|
|
118
|
+
print(f"Stopped after {step} points")
|
|
119
|
+
|
|
120
|
+
print("Done!")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def push_samples_command(args):
|
|
124
|
+
"""Push sample plots to test the server."""
|
|
125
|
+
from grimoireplot.client import push_plot_sync
|
|
126
|
+
from grimoireplot.create_some_plots import (
|
|
127
|
+
create_sample_line_plot,
|
|
128
|
+
create_sample_bar_plot,
|
|
129
|
+
create_sample_scatter_plot,
|
|
130
|
+
create_sample_histogram,
|
|
131
|
+
create_sample_pie_chart,
|
|
132
|
+
create_sample_box_plot,
|
|
133
|
+
create_sample_heatmap,
|
|
134
|
+
create_sample_area_plot,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
server_url = f"http://{args.host}:{args.port}"
|
|
138
|
+
grimoire_name = args.grimoire_name
|
|
139
|
+
|
|
140
|
+
samples = [
|
|
141
|
+
("Basic Plots", "Line Plot", create_sample_line_plot),
|
|
142
|
+
("Basic Plots", "Bar Plot", create_sample_bar_plot),
|
|
143
|
+
("Basic Plots", "Area Plot", create_sample_area_plot),
|
|
144
|
+
("Distributions", "Scatter Plot", create_sample_scatter_plot),
|
|
145
|
+
("Distributions", "Histogram", create_sample_histogram),
|
|
146
|
+
("Distributions", "Box Plot", create_sample_box_plot),
|
|
147
|
+
("Categories", "Pie Chart", create_sample_pie_chart),
|
|
148
|
+
("Categories", "Heatmap", create_sample_heatmap),
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
print(f"Pushing {len(samples)} sample plots to {server_url}")
|
|
152
|
+
print(f"Grimoire: {grimoire_name}")
|
|
153
|
+
print("-" * 40)
|
|
154
|
+
|
|
155
|
+
for chapter, plot_name, create_func in samples:
|
|
156
|
+
try:
|
|
157
|
+
fig = create_func()
|
|
158
|
+
_ = push_plot_sync(
|
|
159
|
+
grimoire_name=grimoire_name,
|
|
160
|
+
chapter_name=chapter,
|
|
161
|
+
plot_name=plot_name,
|
|
162
|
+
fig=fig,
|
|
163
|
+
grimoire_secret=args.secret,
|
|
164
|
+
grimoire_server=server_url,
|
|
165
|
+
)
|
|
166
|
+
print(f" [OK] {chapter}/{plot_name}")
|
|
167
|
+
except Exception as e:
|
|
168
|
+
print(f" [FAIL] {chapter}/{plot_name}: {e}")
|
|
169
|
+
|
|
170
|
+
print("-" * 40)
|
|
171
|
+
print("Done!")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main():
|
|
175
|
+
default_server = get_grimoire_server()
|
|
176
|
+
default_host = default_server.split("://")[-1].split(":")[0]
|
|
177
|
+
default_port = int(default_server.split(":")[-1])
|
|
178
|
+
default_secret = get_grimoire_secret()
|
|
179
|
+
|
|
180
|
+
parser = argparse.ArgumentParser(
|
|
181
|
+
prog="grimoireplot",
|
|
182
|
+
description="GrimoirePlot - Live dashboard for Plotly-compatible plots",
|
|
183
|
+
)
|
|
184
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
185
|
+
|
|
186
|
+
# Serve command
|
|
187
|
+
serve_parser = subparsers.add_parser("serve", help="Start the GrimoirePlot server")
|
|
188
|
+
serve_parser.add_argument(
|
|
189
|
+
"--host",
|
|
190
|
+
type=str,
|
|
191
|
+
default=default_host,
|
|
192
|
+
help=f"Host to bind the server (default: {default_host})",
|
|
193
|
+
)
|
|
194
|
+
serve_parser.add_argument(
|
|
195
|
+
"--port",
|
|
196
|
+
type=int,
|
|
197
|
+
default=default_port,
|
|
198
|
+
help=f"Port to bind the server (default: {default_port})",
|
|
199
|
+
)
|
|
200
|
+
serve_parser.set_defaults(func=serve_command)
|
|
201
|
+
|
|
202
|
+
# Push samples command
|
|
203
|
+
push_parser = subparsers.add_parser(
|
|
204
|
+
"push-samples", help="Push sample plots to test the server"
|
|
205
|
+
)
|
|
206
|
+
push_parser.add_argument(
|
|
207
|
+
"--host",
|
|
208
|
+
type=str,
|
|
209
|
+
default=default_host,
|
|
210
|
+
help=f"Server host (default: {default_host})",
|
|
211
|
+
)
|
|
212
|
+
push_parser.add_argument(
|
|
213
|
+
"--port",
|
|
214
|
+
type=int,
|
|
215
|
+
default=default_port,
|
|
216
|
+
help=f"Server port (default: {default_port})",
|
|
217
|
+
)
|
|
218
|
+
push_parser.add_argument(
|
|
219
|
+
"--secret",
|
|
220
|
+
type=str,
|
|
221
|
+
default=default_secret,
|
|
222
|
+
help="Grimoire secret for authentication",
|
|
223
|
+
)
|
|
224
|
+
push_parser.add_argument(
|
|
225
|
+
"--grimoire-name",
|
|
226
|
+
type=str,
|
|
227
|
+
default="test_grimoire",
|
|
228
|
+
help="Name of the grimoire to create (default: test_grimoire)",
|
|
229
|
+
)
|
|
230
|
+
push_parser.set_defaults(func=push_samples_command)
|
|
231
|
+
|
|
232
|
+
# Live test command
|
|
233
|
+
live_parser = subparsers.add_parser(
|
|
234
|
+
"live-test", help="Test live plot updates by adding datapoints over time"
|
|
235
|
+
)
|
|
236
|
+
live_parser.add_argument(
|
|
237
|
+
"--host",
|
|
238
|
+
type=str,
|
|
239
|
+
default=default_host,
|
|
240
|
+
help=f"Server host (default: {default_host})",
|
|
241
|
+
)
|
|
242
|
+
live_parser.add_argument(
|
|
243
|
+
"--port",
|
|
244
|
+
type=int,
|
|
245
|
+
default=default_port,
|
|
246
|
+
help=f"Server port (default: {default_port})",
|
|
247
|
+
)
|
|
248
|
+
live_parser.add_argument(
|
|
249
|
+
"--secret",
|
|
250
|
+
type=str,
|
|
251
|
+
default=default_secret,
|
|
252
|
+
help="Grimoire secret for authentication",
|
|
253
|
+
)
|
|
254
|
+
live_parser.add_argument(
|
|
255
|
+
"--grimoire-name",
|
|
256
|
+
type=str,
|
|
257
|
+
default="live_test",
|
|
258
|
+
help="Name of the grimoire to create (default: live_test)",
|
|
259
|
+
)
|
|
260
|
+
live_parser.add_argument(
|
|
261
|
+
"--interval",
|
|
262
|
+
type=float,
|
|
263
|
+
default=0.2,
|
|
264
|
+
help="Interval between datapoints in seconds (default: 0.2)",
|
|
265
|
+
)
|
|
266
|
+
live_parser.add_argument(
|
|
267
|
+
"--max-points",
|
|
268
|
+
type=int,
|
|
269
|
+
default=0,
|
|
270
|
+
help="Maximum number of points to add (0 = unlimited, default: 0)",
|
|
271
|
+
)
|
|
272
|
+
live_parser.set_defaults(func=live_test_command)
|
|
273
|
+
|
|
274
|
+
args = parser.parse_args()
|
|
275
|
+
|
|
276
|
+
if args.command is None:
|
|
277
|
+
parser.print_help()
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
args.func(args)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ in {"__main__", "__mp_main__"}:
|
|
284
|
+
main()
|
grimoireplot/models.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright © 2026 Idiap Research Institute <contact@idiap.ch>
|
|
2
|
+
# SPDX-FileContributor: William Droz <william.droz@idiap.ch>
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from sqlmodel import Field, Relationship, SQLModel, create_engine, Session
|
|
9
|
+
from sqlalchemy import ForeignKeyConstraint
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AddPlotRequest(SQLModel):
|
|
16
|
+
grimoire_name: str
|
|
17
|
+
chapter_name: str
|
|
18
|
+
plot_name: str
|
|
19
|
+
json_data: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Grimoire(SQLModel, table=True):
|
|
23
|
+
name: str = Field(primary_key=True)
|
|
24
|
+
|
|
25
|
+
chapters: list["Chapter"] = Relationship(
|
|
26
|
+
back_populates="grimoire", cascade_delete=True
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Chapter(SQLModel, table=True):
|
|
31
|
+
name: str = Field(primary_key=True)
|
|
32
|
+
grimoire_name: str = Field(primary_key=True, foreign_key="grimoire.name")
|
|
33
|
+
|
|
34
|
+
grimoire: Grimoire = Relationship(back_populates="chapters")
|
|
35
|
+
|
|
36
|
+
plots: list["Plot"] = Relationship(back_populates="chapter", cascade_delete=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Plot(SQLModel, table=True):
|
|
40
|
+
__table_args__ = (
|
|
41
|
+
ForeignKeyConstraint(
|
|
42
|
+
["chapter_name", "grimoire_name"], ["chapter.name", "chapter.grimoire_name"]
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
name: str = Field(primary_key=True)
|
|
47
|
+
chapter_name: str = Field(primary_key=True)
|
|
48
|
+
grimoire_name: str = Field(primary_key=True)
|
|
49
|
+
json_data: str
|
|
50
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
51
|
+
|
|
52
|
+
chapter: Chapter = Relationship(back_populates="plots")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_engine():
|
|
56
|
+
if os.getenv("GRIMOIRE_TEST", "").lower() in ("1", "true", "yes"):
|
|
57
|
+
sqlite_file_name = "database-deleteme.db"
|
|
58
|
+
grimoire_db = f"sqlite:///{sqlite_file_name}"
|
|
59
|
+
elif (grimoire_db := os.getenv("GRIMOIRE_DB")) is None:
|
|
60
|
+
sqlite_file_name = "database.db"
|
|
61
|
+
grimoire_db = f"sqlite:///{sqlite_file_name}"
|
|
62
|
+
return create_engine(grimoire_db)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_db_and_tables():
|
|
66
|
+
engine = get_engine()
|
|
67
|
+
SQLModel.metadata.create_all(engine)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_all_grimoires() -> list[Grimoire]:
|
|
71
|
+
"""Get all grimoires with their chapters and plots."""
|
|
72
|
+
engine = get_engine()
|
|
73
|
+
with Session(engine) as session:
|
|
74
|
+
from sqlmodel import select
|
|
75
|
+
|
|
76
|
+
statement = select(Grimoire)
|
|
77
|
+
grimoires = session.exec(statement).all()
|
|
78
|
+
# Ensure relationships are loaded
|
|
79
|
+
result = []
|
|
80
|
+
for grimoire in grimoires:
|
|
81
|
+
# Access chapters to load them
|
|
82
|
+
_ = grimoire.chapters
|
|
83
|
+
for chapter in grimoire.chapters:
|
|
84
|
+
# Access plots to load them
|
|
85
|
+
_ = chapter.plots
|
|
86
|
+
result.append(grimoire)
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_grimoire_with_data(grimoire_name: str) -> Optional[Grimoire]:
|
|
91
|
+
"""Get a specific grimoire with all its chapters and plots."""
|
|
92
|
+
engine = get_engine()
|
|
93
|
+
with Session(engine) as session:
|
|
94
|
+
grimoire = session.get(Grimoire, grimoire_name)
|
|
95
|
+
if grimoire:
|
|
96
|
+
# Load relationships
|
|
97
|
+
_ = grimoire.chapters
|
|
98
|
+
for chapter in grimoire.chapters:
|
|
99
|
+
_ = chapter.plots
|
|
100
|
+
return grimoire
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def add_plot(
|
|
104
|
+
grimoire_name: str, chapter_name: str, plot_name: str, json_data: str
|
|
105
|
+
) -> Plot:
|
|
106
|
+
"""Add a plot to a grimoire/chapter, creating them if they don't exist.
|
|
107
|
+
|
|
108
|
+
If the grimoire exists, use it, else create it.
|
|
109
|
+
If the chapter exists, use it, else create it.
|
|
110
|
+
If the plot exists, replace it, else add it.
|
|
111
|
+
|
|
112
|
+
TODO: check if json_data is valid plotly, if not raise exception.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
engine = get_engine()
|
|
116
|
+
|
|
117
|
+
with Session(engine) as session:
|
|
118
|
+
# Get or create Grimoire
|
|
119
|
+
grimoire = session.get(Grimoire, grimoire_name)
|
|
120
|
+
if grimoire is None:
|
|
121
|
+
grimoire = Grimoire(name=grimoire_name)
|
|
122
|
+
session.add(grimoire)
|
|
123
|
+
session.commit()
|
|
124
|
+
session.refresh(grimoire)
|
|
125
|
+
|
|
126
|
+
# Get or create Chapter
|
|
127
|
+
chapter = session.get(Chapter, (chapter_name, grimoire_name))
|
|
128
|
+
if chapter is None:
|
|
129
|
+
chapter = Chapter(name=chapter_name, grimoire_name=grimoire_name)
|
|
130
|
+
session.add(chapter)
|
|
131
|
+
session.commit()
|
|
132
|
+
session.refresh(chapter)
|
|
133
|
+
|
|
134
|
+
# Get or create/replace Plot
|
|
135
|
+
plot = session.get(Plot, (plot_name, chapter_name, grimoire_name))
|
|
136
|
+
if plot is None:
|
|
137
|
+
plot = Plot(
|
|
138
|
+
name=plot_name,
|
|
139
|
+
chapter_name=chapter_name,
|
|
140
|
+
grimoire_name=grimoire_name,
|
|
141
|
+
json_data=json_data,
|
|
142
|
+
)
|
|
143
|
+
session.add(plot)
|
|
144
|
+
else:
|
|
145
|
+
plot.json_data = json_data
|
|
146
|
+
session.add(plot)
|
|
147
|
+
|
|
148
|
+
session.commit()
|
|
149
|
+
session.refresh(plot)
|
|
150
|
+
|
|
151
|
+
return plot
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def delete_plot(grimoire_name: str, chapter_name: str, plot_name: str) -> bool:
|
|
155
|
+
"""Delete a plot by composite key. Returns True if deleted, False if not found."""
|
|
156
|
+
engine = get_engine()
|
|
157
|
+
with Session(engine) as session:
|
|
158
|
+
plot = session.get(Plot, (plot_name, chapter_name, grimoire_name))
|
|
159
|
+
if plot is None:
|
|
160
|
+
return False
|
|
161
|
+
session.delete(plot)
|
|
162
|
+
session.commit()
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def delete_chapter(grimoire_name: str, chapter_name: str) -> bool:
|
|
167
|
+
"""Delete a chapter and all its plots. Returns True if deleted, False if not found."""
|
|
168
|
+
engine = get_engine()
|
|
169
|
+
with Session(engine) as session:
|
|
170
|
+
chapter = session.get(Chapter, (chapter_name, grimoire_name))
|
|
171
|
+
if chapter is None:
|
|
172
|
+
return False
|
|
173
|
+
session.delete(chapter)
|
|
174
|
+
session.commit()
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def delete_grimoire(grimoire_name: str) -> bool:
|
|
179
|
+
"""Delete a grimoire and all its chapters and plots. Returns True if deleted, False if not found."""
|
|
180
|
+
engine = get_engine()
|
|
181
|
+
with Session(engine) as session:
|
|
182
|
+
grimoire = session.get(Grimoire, grimoire_name)
|
|
183
|
+
if grimoire is None:
|
|
184
|
+
return False
|
|
185
|
+
session.delete(grimoire)
|
|
186
|
+
session.commit()
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_chapter_with_plots(grimoire_name: str, chapter_name: str) -> Optional[Chapter]:
|
|
191
|
+
"""Get a specific chapter with all its plots."""
|
|
192
|
+
engine = get_engine()
|
|
193
|
+
with Session(engine) as session:
|
|
194
|
+
chapter = session.get(Chapter, (chapter_name, grimoire_name))
|
|
195
|
+
if chapter:
|
|
196
|
+
# Load relationships
|
|
197
|
+
_ = chapter.plots
|
|
198
|
+
return chapter
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_plots_for_chapter(grimoire_name: str, chapter_name: str) -> list[Plot]:
|
|
202
|
+
"""Get all plots for a specific chapter."""
|
|
203
|
+
engine = get_engine()
|
|
204
|
+
with Session(engine) as session:
|
|
205
|
+
from sqlmodel import select
|
|
206
|
+
|
|
207
|
+
statement = select(Plot).where(
|
|
208
|
+
Plot.chapter_name == chapter_name, Plot.grimoire_name == grimoire_name
|
|
209
|
+
)
|
|
210
|
+
return list(session.exec(statement).all())
|
grimoireplot/server.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright © 2026 Idiap Research Institute <contact@idiap.ch>
|
|
2
|
+
# SPDX-FileContributor: William Droz <william.droz@idiap.ch>
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
from fastapi import HTTPException, Request
|
|
6
|
+
from nicegui import app, ui
|
|
7
|
+
from grimoireplot.common import get_grimoire_secret
|
|
8
|
+
from grimoireplot.models import (
|
|
9
|
+
AddPlotRequest,
|
|
10
|
+
create_db_and_tables,
|
|
11
|
+
add_plot,
|
|
12
|
+
delete_plot,
|
|
13
|
+
delete_chapter,
|
|
14
|
+
delete_grimoire,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from grimoireplot.ui import dashboard_ui, refresh_chapter_plots
|
|
18
|
+
from grimoireplot.ui_elements import setup_theme
|
|
19
|
+
|
|
20
|
+
_GRIMOIRE_SECRET = get_grimoire_secret()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def verify_secret(request: Request):
|
|
24
|
+
if (secret := request.headers.get("grimoire-secret")) is None:
|
|
25
|
+
raise HTTPException(status_code=401, detail="grimoire-secret not found")
|
|
26
|
+
if secret != _GRIMOIRE_SECRET:
|
|
27
|
+
raise HTTPException(status_code=403, detail="invalid grimoire-secret")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def my_app(host: str = "localhost", port: int = 8080):
|
|
31
|
+
create_db_and_tables()
|
|
32
|
+
|
|
33
|
+
@app.post("/add_plot")
|
|
34
|
+
def add_plot_endpoint(add_plot_request: AddPlotRequest, request: Request):
|
|
35
|
+
verify_secret(request)
|
|
36
|
+
plot = add_plot(
|
|
37
|
+
grimoire_name=add_plot_request.grimoire_name,
|
|
38
|
+
chapter_name=add_plot_request.chapter_name,
|
|
39
|
+
plot_name=add_plot_request.plot_name,
|
|
40
|
+
json_data=add_plot_request.json_data,
|
|
41
|
+
)
|
|
42
|
+
# Try to refresh only the specific chapter's plots
|
|
43
|
+
# If the chapter doesn't exist in UI yet (new grimoire/chapter), refresh the whole dashboard
|
|
44
|
+
if not refresh_chapter_plots(
|
|
45
|
+
add_plot_request.grimoire_name, add_plot_request.chapter_name
|
|
46
|
+
):
|
|
47
|
+
dashboard_ui.refresh()
|
|
48
|
+
return {"status": "success", "plot_name": plot.name}
|
|
49
|
+
|
|
50
|
+
@app.delete("/grimoire/{grimoire_name}/chapter/{chapter_name}/plot/{plot_name}")
|
|
51
|
+
def delete_plot_endpoint(
|
|
52
|
+
grimoire_name: str, chapter_name: str, plot_name: str, request: Request
|
|
53
|
+
):
|
|
54
|
+
verify_secret(request)
|
|
55
|
+
if not delete_plot(grimoire_name, chapter_name, plot_name):
|
|
56
|
+
raise HTTPException(status_code=404, detail="Plot not found")
|
|
57
|
+
dashboard_ui.refresh()
|
|
58
|
+
return {"status": "success", "deleted": plot_name}
|
|
59
|
+
|
|
60
|
+
@app.delete("/grimoire/{grimoire_name}/chapter/{chapter_name}")
|
|
61
|
+
def delete_chapter_endpoint(
|
|
62
|
+
grimoire_name: str, chapter_name: str, request: Request
|
|
63
|
+
):
|
|
64
|
+
verify_secret(request)
|
|
65
|
+
if not delete_chapter(grimoire_name, chapter_name):
|
|
66
|
+
raise HTTPException(status_code=404, detail="Chapter not found")
|
|
67
|
+
dashboard_ui.refresh()
|
|
68
|
+
return {"status": "success", "deleted": chapter_name}
|
|
69
|
+
|
|
70
|
+
@app.delete("/grimoire/{grimoire_name}")
|
|
71
|
+
def delete_grimoire_endpoint(grimoire_name: str, request: Request):
|
|
72
|
+
verify_secret(request)
|
|
73
|
+
if not delete_grimoire(grimoire_name):
|
|
74
|
+
raise HTTPException(status_code=404, detail="Grimoire not found")
|
|
75
|
+
dashboard_ui.refresh()
|
|
76
|
+
return {"status": "success", "deleted": grimoire_name}
|
|
77
|
+
|
|
78
|
+
@ui.page("/")
|
|
79
|
+
def page():
|
|
80
|
+
setup_theme()
|
|
81
|
+
dashboard_ui()
|
|
82
|
+
|
|
83
|
+
ui.run(
|
|
84
|
+
host=host,
|
|
85
|
+
port=port,
|
|
86
|
+
dark=True,
|
|
87
|
+
title="GrimoirePlot - Data Visualization Dashboard",
|
|
88
|
+
favicon="🔮",
|
|
89
|
+
reload=False,
|
|
90
|
+
)
|