grimoireplot 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.3
2
+ Name: grimoireplot
3
+ Version: 0.0.1
4
+ Summary: GrimoirePlot is a live dashboard of plotly-compatible plots of remote data
5
+ Author: William Droz
6
+ Author-email: William Droz <william.droz@idiap.ch>
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Requires-Dist: aiohttp[speedups]>=3.13.3
9
+ Requires-Dist: loguru>=0.7.3
10
+ Requires-Dist: nicegui>=3.5.0,<4
11
+ Requires-Dist: plotly>=6.5.2
12
+ Requires-Dist: python-dotenv>=1.2.1
13
+ Requires-Dist: requests>=2.32.5
14
+ Requires-Dist: sqlmodel>=0.0.31
15
+ Requires-Dist: pytest>=8.0 ; extra == 'dev'
16
+ Requires-Python: >=3.11
17
+ Project-URL: Home, https://github.com/idiap/GrimoirePlot
18
+ Provides-Extra: dev
19
+ Description-Content-Type: text/markdown
20
+
21
+ # GrimoirePlot
22
+
23
+ *GrimoirePlot is a live dashboard of plotly-compatible plots of remote data*
24
+
25
+ ![demo](media/demo_grimoire.gif)
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ uv pip install grimoireplot # not yet on pypi, will setup ci/cd on github later on
31
+ ```
32
+
33
+ Or install from source:
34
+
35
+ ```bash
36
+ # git clone the repo
37
+ cd grimoireplot
38
+ uv sync --extra dev
39
+ ```
40
+
41
+ ### Installation as a tool
42
+
43
+ ```bash
44
+ uv tool install grimoireplot # not yet on pypi, will setup ci/cd on github later on
45
+ ```
46
+
47
+ ## Quick Start
48
+
49
+ ### 1. Start the Server
50
+
51
+ ```bash
52
+ grimoireplot serve --host localhost --port 8080
53
+ ```
54
+
55
+ Then open your browser at `http://localhost:8080` to see the dashboard.
56
+
57
+ ### 2. Push Sample Plots (Test the Server)
58
+
59
+ In another terminal, push some sample plots to verify everything works:
60
+
61
+ ```bash
62
+ grimoireplot push-samples --host localhost --port 8080
63
+ ```
64
+
65
+ ## CLI Reference
66
+
67
+ ### `grimoireplot serve`
68
+
69
+ Start the GrimoirePlot dashboard server.
70
+
71
+ ```bash
72
+ grimoireplot serve [--host HOST] [--port PORT]
73
+ ```
74
+
75
+ | Option | Default | Description |
76
+ |--------|---------|-------------|
77
+ | `--host` | `localhost` | Host to bind the server |
78
+ | `--port` | `8080` | Port to bind the server |
79
+
80
+ ### `grimoireplot push-samples`
81
+
82
+ Push sample plots to test the server.
83
+
84
+ ```bash
85
+ grimoireplot push-samples [--host HOST] [--port PORT] [--secret SECRET] [--grimoire-name NAME]
86
+ ```
87
+
88
+ | Option | Default | Description |
89
+ |--------|---------|-------------|
90
+ | `--host` | `localhost` | Server host |
91
+ | `--port` | `8080` | Server port |
92
+ | `--secret` | `IDidntSetASecret` | Authentication secret |
93
+ | `--grimoire-name` | `test_grimoire` | Name of the grimoire to create |
94
+
95
+ ### `grimoireplot live-test`
96
+
97
+ Test live plot updates by continuously adding datapoints to a line plot.
98
+
99
+ ```bash
100
+ grimoireplot live-test [--host HOST] [--port PORT] [--secret SECRET] [--grimoire-name NAME] [--interval SECONDS] [--max-points N]
101
+ ```
102
+
103
+ | Option | Default | Description |
104
+ |--------|---------|-------------|
105
+ | `--host` | `localhost` | Server host |
106
+ | `--port` | `8080` | Server port |
107
+ | `--secret` | `IDidntSetASecret` | Authentication secret |
108
+ | `--grimoire-name` | `live_test` | Name of the grimoire to create |
109
+ | `--interval` | `0.2` | Interval between datapoints in seconds |
110
+ | `--max-points` | `0` | Maximum number of points (0 = unlimited) |
111
+
112
+ ## Programmatic Usage
113
+
114
+ ### Sending Plots from Python
115
+
116
+ GrimoirePlot organizes plots in a hierarchy: **Grimoire** → **Chapter** → **Plot**
117
+
118
+ #### Synchronous API
119
+
120
+ ```python
121
+ import plotly.graph_objects as go
122
+ from grimoireplot.client import push_plot_sync
123
+
124
+ # Create a Plotly figure
125
+ fig = go.Figure()
126
+ fig.add_trace(go.Scatter(x=[1, 2, 3, 4], y=[10, 11, 12, 13], mode='lines+markers'))
127
+ fig.update_layout(title='My Plot')
128
+
129
+ # Push to the server
130
+ response = push_plot_sync(
131
+ grimoire_name="my_experiment",
132
+ chapter_name="training_metrics",
133
+ plot_name="loss_curve",
134
+ fig=fig,
135
+ grimoire_server="http://localhost:8080",
136
+ grimoire_secret="your-secret",
137
+ )
138
+ ```
139
+
140
+ #### Asynchronous API
141
+
142
+ ```python
143
+ import asyncio
144
+ import plotly.graph_objects as go
145
+ from grimoireplot.client import push_plot
146
+
147
+ async def main():
148
+ fig = go.Figure()
149
+ fig.add_trace(go.Bar(x=['A', 'B', 'C'], y=[20, 14, 23]))
150
+ fig.update_layout(title='Async Plot')
151
+
152
+ response = await push_plot(
153
+ grimoire_name="my_experiment",
154
+ chapter_name="results",
155
+ plot_name="bar_chart",
156
+ fig=fig,
157
+ grimoire_server="http://localhost:8080",
158
+ grimoire_secret="your-secret",
159
+ )
160
+
161
+ asyncio.run(main())
162
+ ```
163
+
164
+ ### Integration Example: Training Loop
165
+
166
+ ```python
167
+ import plotly.graph_objects as go
168
+ from grimoireplot.client import push_plot_sync
169
+
170
+ losses = []
171
+
172
+ for epoch in range(100):
173
+ loss = train_one_epoch() # Your training code
174
+ losses.append(loss)
175
+
176
+ # Update the plot every 10 epochs
177
+ if epoch % 10 == 0:
178
+ fig = go.Figure()
179
+ fig.add_trace(go.Scatter(y=losses, mode='lines', name='Training Loss'))
180
+ fig.update_layout(title=f'Training Progress (Epoch {epoch})',
181
+ xaxis_title='Epoch', yaxis_title='Loss')
182
+
183
+ push_plot_sync(
184
+ grimoire_name="experiment_001",
185
+ chapter_name="training",
186
+ plot_name="loss",
187
+ fig=fig,
188
+ )
189
+ ```
190
+
191
+ ## Configuration
192
+
193
+ GrimoirePlot can be configured via environment variables:
194
+
195
+ | Variable | Default | Description |
196
+ |----------|---------|-------------|
197
+ | `GRIMOIRE_SERVER` | `http://localhost:8080` | Default server URL |
198
+ | `GRIMOIRE_SECRET` | `IDidntSetASecret` | Authentication secret |
199
+
200
+ You can also use a `.env` file in your project directory.
201
+
202
+ ## Testing
203
+
204
+ ```bash
205
+ # you need to install with --extra dev
206
+ GRIMOIRE_TEST=true uv run pytest
207
+ ```
208
+
209
+ ## Concepts
210
+
211
+ - **Grimoire**: A collection of related visualizations (e.g., an experiment)
212
+ - **Chapter**: A group of plots within a grimoire (e.g., training metrics, evaluation results)
213
+ - **Plot**: A single Plotly figure
214
+
215
+ ## Acknowledgments
216
+
217
+ GrimoirePlot is inspired by [visdom](https://github.com/fossasia/visdom)
@@ -0,0 +1,197 @@
1
+ # GrimoirePlot
2
+
3
+ *GrimoirePlot is a live dashboard of plotly-compatible plots of remote data*
4
+
5
+ ![demo](media/demo_grimoire.gif)
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ uv pip install grimoireplot # not yet on pypi, will setup ci/cd on github later on
11
+ ```
12
+
13
+ Or install from source:
14
+
15
+ ```bash
16
+ # git clone the repo
17
+ cd grimoireplot
18
+ uv sync --extra dev
19
+ ```
20
+
21
+ ### Installation as a tool
22
+
23
+ ```bash
24
+ uv tool install grimoireplot # not yet on pypi, will setup ci/cd on github later on
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### 1. Start the Server
30
+
31
+ ```bash
32
+ grimoireplot serve --host localhost --port 8080
33
+ ```
34
+
35
+ Then open your browser at `http://localhost:8080` to see the dashboard.
36
+
37
+ ### 2. Push Sample Plots (Test the Server)
38
+
39
+ In another terminal, push some sample plots to verify everything works:
40
+
41
+ ```bash
42
+ grimoireplot push-samples --host localhost --port 8080
43
+ ```
44
+
45
+ ## CLI Reference
46
+
47
+ ### `grimoireplot serve`
48
+
49
+ Start the GrimoirePlot dashboard server.
50
+
51
+ ```bash
52
+ grimoireplot serve [--host HOST] [--port PORT]
53
+ ```
54
+
55
+ | Option | Default | Description |
56
+ |--------|---------|-------------|
57
+ | `--host` | `localhost` | Host to bind the server |
58
+ | `--port` | `8080` | Port to bind the server |
59
+
60
+ ### `grimoireplot push-samples`
61
+
62
+ Push sample plots to test the server.
63
+
64
+ ```bash
65
+ grimoireplot push-samples [--host HOST] [--port PORT] [--secret SECRET] [--grimoire-name NAME]
66
+ ```
67
+
68
+ | Option | Default | Description |
69
+ |--------|---------|-------------|
70
+ | `--host` | `localhost` | Server host |
71
+ | `--port` | `8080` | Server port |
72
+ | `--secret` | `IDidntSetASecret` | Authentication secret |
73
+ | `--grimoire-name` | `test_grimoire` | Name of the grimoire to create |
74
+
75
+ ### `grimoireplot live-test`
76
+
77
+ Test live plot updates by continuously adding datapoints to a line plot.
78
+
79
+ ```bash
80
+ grimoireplot live-test [--host HOST] [--port PORT] [--secret SECRET] [--grimoire-name NAME] [--interval SECONDS] [--max-points N]
81
+ ```
82
+
83
+ | Option | Default | Description |
84
+ |--------|---------|-------------|
85
+ | `--host` | `localhost` | Server host |
86
+ | `--port` | `8080` | Server port |
87
+ | `--secret` | `IDidntSetASecret` | Authentication secret |
88
+ | `--grimoire-name` | `live_test` | Name of the grimoire to create |
89
+ | `--interval` | `0.2` | Interval between datapoints in seconds |
90
+ | `--max-points` | `0` | Maximum number of points (0 = unlimited) |
91
+
92
+ ## Programmatic Usage
93
+
94
+ ### Sending Plots from Python
95
+
96
+ GrimoirePlot organizes plots in a hierarchy: **Grimoire** → **Chapter** → **Plot**
97
+
98
+ #### Synchronous API
99
+
100
+ ```python
101
+ import plotly.graph_objects as go
102
+ from grimoireplot.client import push_plot_sync
103
+
104
+ # Create a Plotly figure
105
+ fig = go.Figure()
106
+ fig.add_trace(go.Scatter(x=[1, 2, 3, 4], y=[10, 11, 12, 13], mode='lines+markers'))
107
+ fig.update_layout(title='My Plot')
108
+
109
+ # Push to the server
110
+ response = push_plot_sync(
111
+ grimoire_name="my_experiment",
112
+ chapter_name="training_metrics",
113
+ plot_name="loss_curve",
114
+ fig=fig,
115
+ grimoire_server="http://localhost:8080",
116
+ grimoire_secret="your-secret",
117
+ )
118
+ ```
119
+
120
+ #### Asynchronous API
121
+
122
+ ```python
123
+ import asyncio
124
+ import plotly.graph_objects as go
125
+ from grimoireplot.client import push_plot
126
+
127
+ async def main():
128
+ fig = go.Figure()
129
+ fig.add_trace(go.Bar(x=['A', 'B', 'C'], y=[20, 14, 23]))
130
+ fig.update_layout(title='Async Plot')
131
+
132
+ response = await push_plot(
133
+ grimoire_name="my_experiment",
134
+ chapter_name="results",
135
+ plot_name="bar_chart",
136
+ fig=fig,
137
+ grimoire_server="http://localhost:8080",
138
+ grimoire_secret="your-secret",
139
+ )
140
+
141
+ asyncio.run(main())
142
+ ```
143
+
144
+ ### Integration Example: Training Loop
145
+
146
+ ```python
147
+ import plotly.graph_objects as go
148
+ from grimoireplot.client import push_plot_sync
149
+
150
+ losses = []
151
+
152
+ for epoch in range(100):
153
+ loss = train_one_epoch() # Your training code
154
+ losses.append(loss)
155
+
156
+ # Update the plot every 10 epochs
157
+ if epoch % 10 == 0:
158
+ fig = go.Figure()
159
+ fig.add_trace(go.Scatter(y=losses, mode='lines', name='Training Loss'))
160
+ fig.update_layout(title=f'Training Progress (Epoch {epoch})',
161
+ xaxis_title='Epoch', yaxis_title='Loss')
162
+
163
+ push_plot_sync(
164
+ grimoire_name="experiment_001",
165
+ chapter_name="training",
166
+ plot_name="loss",
167
+ fig=fig,
168
+ )
169
+ ```
170
+
171
+ ## Configuration
172
+
173
+ GrimoirePlot can be configured via environment variables:
174
+
175
+ | Variable | Default | Description |
176
+ |----------|---------|-------------|
177
+ | `GRIMOIRE_SERVER` | `http://localhost:8080` | Default server URL |
178
+ | `GRIMOIRE_SECRET` | `IDidntSetASecret` | Authentication secret |
179
+
180
+ You can also use a `.env` file in your project directory.
181
+
182
+ ## Testing
183
+
184
+ ```bash
185
+ # you need to install with --extra dev
186
+ GRIMOIRE_TEST=true uv run pytest
187
+ ```
188
+
189
+ ## Concepts
190
+
191
+ - **Grimoire**: A collection of related visualizations (e.g., an experiment)
192
+ - **Chapter**: A group of plots within a grimoire (e.g., training metrics, evaluation results)
193
+ - **Plot**: A single Plotly figure
194
+
195
+ ## Acknowledgments
196
+
197
+ GrimoirePlot is inspired by [visdom](https://github.com/fossasia/visdom)
@@ -0,0 +1,5 @@
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
+ """Module for GrimoirePlot"""
@@ -0,0 +1,166 @@
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
+ Client module that push plots
7
+ """
8
+
9
+ from plotly.graph_objects import Figure
10
+ import aiohttp
11
+ import requests
12
+ from grimoireplot.common import get_grimoire_secret, get_grimoire_server
13
+
14
+
15
+ default_secret = get_grimoire_secret()
16
+ default_server = get_grimoire_server()
17
+
18
+
19
+ def push_plot_sync(
20
+ grimoire_name: str,
21
+ chapter_name: str,
22
+ plot_name: str,
23
+ fig: Figure,
24
+ grimoire_secret: str = default_secret,
25
+ grimoire_server: str = default_server,
26
+ ) -> dict:
27
+ """Push a plot to the grimoire server.
28
+
29
+ Args:
30
+ grimoire_name (str): Name of the grimoire.
31
+ chapter_name (str): Name of the chapter.
32
+ plot_name (str): Name of the plot.
33
+ fig (Figure): Plotly figure to push.
34
+ grimoire_secret (str, optional): Secret for authentication. Defaults to default_secret.
35
+ grimoire_server (str, optional): Grimoire server URL. Defaults to default_server.
36
+
37
+ Returns:
38
+ dict: Response from the server.
39
+ """
40
+ json_data = fig.to_json()
41
+ match json_data:
42
+ case str():
43
+ return push_plot_json_sync(
44
+ grimoire_name=grimoire_name,
45
+ chapter_name=chapter_name,
46
+ plot_name=plot_name,
47
+ json_data=json_data,
48
+ grimoire_secret=grimoire_secret,
49
+ grimoire_server=grimoire_server,
50
+ )
51
+ case _:
52
+ raise ValueError(
53
+ "fig.to_json() did not return a string, maybe fig is invalid?"
54
+ )
55
+
56
+
57
+ def push_plot_json_sync(
58
+ grimoire_name: str,
59
+ chapter_name: str,
60
+ plot_name: str,
61
+ json_data: str,
62
+ grimoire_secret: str = default_secret,
63
+ grimoire_server: str = default_server,
64
+ ) -> dict:
65
+ """Push a plot to the grimoire server.
66
+
67
+ Args:
68
+ grimoire_name (str): Name of the grimoire.
69
+ chapter_name (str): Name of the chapter.
70
+ plot_name (str): Name of the plot.
71
+ json_data (str): JSON representation of the plotly figure.
72
+ grimoire_secret (str, optional): Secret for authentication. Defaults to default_secret.
73
+ grimoire_server (str, optional): Grimoire server URL. Defaults to defualt_server.
74
+
75
+ Returns:
76
+ dict: Response from the server.
77
+ """
78
+
79
+ url = f"{grimoire_server}/add_plot"
80
+ headers = {"grimoire-secret": grimoire_secret}
81
+
82
+ payload = {
83
+ "grimoire_name": grimoire_name,
84
+ "chapter_name": chapter_name,
85
+ "plot_name": plot_name,
86
+ "json_data": json_data,
87
+ }
88
+
89
+ response = requests.post(url, headers=headers, json=payload)
90
+ response.raise_for_status()
91
+ return response.json()
92
+
93
+
94
+ async def push_plot(
95
+ grimoire_name: str,
96
+ chapter_name: str,
97
+ plot_name: str,
98
+ fig: Figure,
99
+ grimoire_secret: str = default_secret,
100
+ grimoire_server: str = default_server,
101
+ ) -> dict:
102
+ """Push a plot to the grimoire server asynchronously.
103
+
104
+ Args:
105
+ grimoire_name (str): Name of the grimoire.
106
+ chapter_name (str): Name of the chapter.
107
+ plot_name (str): Name of the plot.
108
+ fig (Figure): Plotly figure to push.
109
+ grimoire_secret (str, optional): Secret for authentication. Defaults to default_secret.
110
+ grimoire_server (str, optional): Grimoire server URL. Defaults to default_server.
111
+
112
+ Returns:
113
+ dict: Response from the server.
114
+ """
115
+ json_data = fig.to_json()
116
+ match json_data:
117
+ case str():
118
+ return await push_plot_json(
119
+ grimoire_name=grimoire_name,
120
+ chapter_name=chapter_name,
121
+ plot_name=plot_name,
122
+ json_data=json_data,
123
+ grimoire_secret=grimoire_secret,
124
+ grimoire_server=grimoire_server,
125
+ )
126
+ case _:
127
+ raise ValueError(
128
+ "fig.to_json() did not return a string, maybe fig is invalid?"
129
+ )
130
+
131
+
132
+ async def push_plot_json(
133
+ grimoire_name: str,
134
+ chapter_name: str,
135
+ plot_name: str,
136
+ json_data: str,
137
+ grimoire_secret: str = default_secret,
138
+ grimoire_server: str = default_server,
139
+ ) -> dict:
140
+ """Push a plot to the grimoire server asynchronously.
141
+
142
+ Args:
143
+ grimoire_name (str): Name of the grimoire.
144
+ chapter_name (str): Name of the chapter.
145
+ plot_name (str): Name of the plot.
146
+ json_data (str): JSON representation of the plotly figure.
147
+ grimoire_secret (str, optional): Secret for authentication. Defaults to default_secret.
148
+ grimoire_server (str, optional): Grimoire server URL. Defaults to default_server.
149
+
150
+ Returns:
151
+ dict: Response from the server.
152
+ """
153
+ url = f"{grimoire_server}/add_plot"
154
+ headers = {"grimoire-secret": grimoire_secret}
155
+
156
+ payload = {
157
+ "grimoire_name": grimoire_name,
158
+ "chapter_name": chapter_name,
159
+ "plot_name": plot_name,
160
+ "json_data": json_data,
161
+ }
162
+
163
+ async with aiohttp.ClientSession() as session:
164
+ async with session.post(url, headers=headers, json=payload) as response:
165
+ response.raise_for_status()
166
+ return await response.json()
@@ -0,0 +1,32 @@
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
+ Common module for grimoireplot client and server
7
+ """
8
+
9
+ import os
10
+ from dotenv import load_dotenv
11
+ from loguru import logger
12
+
13
+
14
+ load_dotenv()
15
+
16
+
17
+ def get_grimoire_secret() -> str:
18
+ if (grimoire_secret := os.environ.get("GRIMOIRE_SECRET")) is None:
19
+ logger.warning("GRIMOIRE_SECRET not set; using default secret")
20
+ grimoire_secret = "IDidntSetASecret"
21
+
22
+ return grimoire_secret
23
+
24
+
25
+ def get_grimoire_server() -> str:
26
+ if (grimoire_server := os.environ.get("GRIMOIRE_SERVER")) is None:
27
+ grimoire_server = "http://localhost:8080"
28
+ logger.warning(
29
+ f"GRIMOIRE_SERVER not set; using default server {grimoire_server}"
30
+ )
31
+
32
+ return grimoire_server