grimoireplot 0.0.1__tar.gz → 0.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: grimoireplot
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: GrimoirePlot is a live dashboard of plotly-compatible plots of remote data
5
5
  Author: William Droz
6
6
  Author-email: William Droz <william.droz@idiap.ch>
@@ -24,10 +24,10 @@ Description-Content-Type: text/markdown
24
24
 
25
25
  ![demo](media/demo_grimoire.gif)
26
26
 
27
- ## Installation
27
+ ## Adding to your current project
28
28
 
29
29
  ```bash
30
- uv pip install grimoireplot # not yet on pypi, will setup ci/cd on github later on
30
+ uv add grimoireplot
31
31
  ```
32
32
 
33
33
  Or install from source:
@@ -4,10 +4,10 @@
4
4
 
5
5
  ![demo](media/demo_grimoire.gif)
6
6
 
7
- ## Installation
7
+ ## Adding to your current project
8
8
 
9
9
  ```bash
10
- uv pip install grimoireplot # not yet on pypi, will setup ci/cd on github later on
10
+ uv add grimoireplot
11
11
  ```
12
12
 
13
13
  Or install from source:
@@ -7,6 +7,7 @@ from typing import Optional
7
7
  from datetime import datetime
8
8
  from sqlmodel import Field, Relationship, SQLModel, create_engine, Session
9
9
  from sqlalchemy import ForeignKeyConstraint
10
+ from sqlalchemy.exc import IntegrityError
10
11
  from dotenv import load_dotenv
11
12
 
12
13
  load_dotenv()
@@ -118,36 +119,56 @@ def add_plot(
118
119
  # Get or create Grimoire
119
120
  grimoire = session.get(Grimoire, grimoire_name)
120
121
  if grimoire is None:
121
- grimoire = Grimoire(name=grimoire_name)
122
- session.add(grimoire)
123
- session.commit()
124
- session.refresh(grimoire)
122
+ try:
123
+ grimoire = Grimoire(name=grimoire_name)
124
+ session.add(grimoire)
125
+ session.commit()
126
+ session.refresh(grimoire)
127
+ except IntegrityError:
128
+ session.rollback()
129
+ grimoire = session.get(Grimoire, grimoire_name)
125
130
 
126
131
  # Get or create Chapter
127
132
  chapter = session.get(Chapter, (chapter_name, grimoire_name))
128
133
  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)
134
+ try:
135
+ chapter = Chapter(name=chapter_name, grimoire_name=grimoire_name)
136
+ session.add(chapter)
137
+ session.commit()
138
+ session.refresh(chapter)
139
+ except IntegrityError:
140
+ session.rollback()
141
+ chapter = session.get(Chapter, (chapter_name, grimoire_name))
133
142
 
134
143
  # Get or create/replace Plot
135
144
  plot = session.get(Plot, (plot_name, chapter_name, grimoire_name))
136
145
  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)
146
+ try:
147
+ plot = Plot(
148
+ name=plot_name,
149
+ chapter_name=chapter_name,
150
+ grimoire_name=grimoire_name,
151
+ json_data=json_data,
152
+ )
153
+ session.add(plot)
154
+ session.commit()
155
+ session.refresh(plot)
156
+ except IntegrityError:
157
+ session.rollback()
158
+ plot = session.get(Plot, (plot_name, chapter_name, grimoire_name))
159
+ if plot is not None:
160
+ plot.json_data = json_data
161
+ session.add(plot)
162
+ session.commit()
163
+ session.refresh(plot)
144
164
  else:
145
165
  plot.json_data = json_data
146
166
  session.add(plot)
167
+ session.commit()
168
+ session.refresh(plot)
147
169
 
148
- session.commit()
149
- session.refresh(plot)
150
-
170
+ if plot is None:
171
+ raise RuntimeError("Failed to create or retrieve plot")
151
172
  return plot
152
173
 
153
174
 
@@ -2,6 +2,10 @@
2
2
  # SPDX-FileContributor: William Droz <william.droz@idiap.ch>
3
3
  # SPDX-License-Identifier: MIT
4
4
 
5
+ import time
6
+ import logging
7
+ from functools import wraps
8
+
5
9
  from fastapi import HTTPException, Request
6
10
  from nicegui import app, ui
7
11
  from grimoireplot.common import get_grimoire_secret
@@ -19,6 +23,44 @@ from grimoireplot.ui_elements import setup_theme
19
23
 
20
24
  _GRIMOIRE_SECRET = get_grimoire_secret()
21
25
 
26
+ logger = logging.getLogger("grimoireplot")
27
+
28
+
29
+ def retry_on_db_error(max_retries: int = 5, base_delay: float = 0.1):
30
+ """Decorator that retries a function on database errors up to max_retries times."""
31
+
32
+ def decorator(func):
33
+ @wraps(func)
34
+ def wrapper(*args, **kwargs):
35
+ for attempt in range(1, max_retries + 1):
36
+ try:
37
+ return func(*args, **kwargs)
38
+ except HTTPException:
39
+ raise
40
+ except Exception as e:
41
+ if attempt < max_retries:
42
+ delay = base_delay * (2 ** (attempt - 1))
43
+ logger.warning(
44
+ "Database error on attempt %d/%d: %s. Retrying in %.2fs...",
45
+ attempt,
46
+ max_retries,
47
+ str(e),
48
+ delay,
49
+ )
50
+ time.sleep(delay)
51
+ else:
52
+ logger.error(
53
+ "Database error on attempt %d/%d: %s. No more retries.",
54
+ attempt,
55
+ max_retries,
56
+ str(e),
57
+ )
58
+ raise
59
+
60
+ return wrapper
61
+
62
+ return decorator
63
+
22
64
 
23
65
  def verify_secret(request: Request):
24
66
  if (secret := request.headers.get("grimoire-secret")) is None:
@@ -31,6 +73,7 @@ def my_app(host: str = "localhost", port: int = 8080):
31
73
  create_db_and_tables()
32
74
 
33
75
  @app.post("/add_plot")
76
+ @retry_on_db_error(max_retries=5)
34
77
  def add_plot_endpoint(add_plot_request: AddPlotRequest, request: Request):
35
78
  verify_secret(request)
36
79
  plot = add_plot(
@@ -8,7 +8,7 @@ build-backend = "uv_build"
8
8
 
9
9
  [project]
10
10
  name = "grimoireplot"
11
- version = "0.0.1"
11
+ version = "0.0.2"
12
12
  description = "GrimoirePlot is a live dashboard of plotly-compatible plots of remote data"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.11"