Compare commits

...

2 Commits

Author SHA1 Message Date
edo-neo
9fdd0dd977 split db pop_up and semi auto fix 2025-09-23 15:19:55 +02:00
edo-neo
9d5c935fd2 spit db solution tbt 2025-09-23 14:26:05 +02:00
6 changed files with 294 additions and 24 deletions

View File

@ -223,7 +223,7 @@ class ArchiveSynchronizer(Component):
"result": "OK" if record.result else "KO",
"serial": record.id,
"time": record.time.isoformat(),
"user": record.user.username,
"user": (record.user.username if hasattr(record.user, "username") else record.user),
"barcode_out": record.barcode if record.barcode else "NA",
}, timeout=5, verify=False)
else:
@ -234,7 +234,7 @@ class ArchiveSynchronizer(Component):
"result": "OK" if record.result else "KO",
"serial": record.id,
"time": record.time.isoformat(),
"user": record.user.username,
"user": (record.user.username if hasattr(record.user, "username") else record.user),
}, timeout=5, verify=False)
if r.status_code != 200:

View File

@ -3,11 +3,15 @@ import csv
import json
import logging
import re
import os
import sys
from playhouse.sqlite_ext import JSONField
from .models import Archive, Log, Recipes, Session, Users, db
from .models import Archive, Log, Recipes, Session, Users, db, db_archive
from .models.base_model import handle_fatal_sqlite_error
# Keep a unified reference for consumers like Crud_DB
models_reference = {
"archive": Archive,
"log": Log,
@ -15,36 +19,94 @@ models_reference = {
"users": Users,
}
db.connect()
db.create_tables(list(models_reference.values()))
# Optional test flag to simulate on-disk SQLite corruption (archive DB only)
if "--kill-db" in sys.argv:
try:
_db = db_archive # Only corrupt the archive database
path = getattr(_db, "database", None)
if path:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(b"NOT A DATABASE - corrupted for test")
except Exception:
pass
# Connect both databases and create tables in their respective DBs
try:
db.connect()
db_archive.connect()
except Exception as e:
msg = str(e).lower()
if any(s in msg for s in (
"malformed",
"not a database",
"file is encrypted or is not a database",
"database disk image is malformed",
)):
handle_fatal_sqlite_error(e)
raise
# Create tables for the main database (exclude Archive which lives in db_archive)
main_models = [Log, Recipes, Users]
db.create_tables(main_models)
# Create tables for the archive database
archive_models = [Archive]
db_archive.create_tables(archive_models)
log = logging.getLogger("db")
@db.atomic()
def init_db():
# Import seed data for main DB
tables = db.get_tables()
tables.sort()
for table in tables:
count = 0
try:
with open(f"src/lib/db/imports/{table}.csv", "r") as f:
table = models_reference[table]
fields = list(table._meta.fields)
log.info(f"importing {table._meta.table_name}")
table_model = models_reference[table]
fields = list(table_model._meta.fields)
log.info(f"importing {table_model._meta.table_name}")
reader = csv.DictReader(f)
for row in reader:
obj = {}
for field in fields:
if type(table._meta.fields[field]) is JSONField:
if type(table_model._meta.fields[field]) is JSONField:
obj[field] = json.loads(row[field])
else:
try:
obj[field] = ast.literal_eval(row[field])
except (SyntaxError, ValueError):
obj[field] = row[field]
table.insert(**obj).on_conflict_replace().execute()
table_model.insert(**obj).on_conflict_replace().execute()
count += 1
log.info(f"{table._meta.table_name}: imported {count} rows.")
log.info(f"{table_model._meta.table_name}: imported {count} rows.")
except FileNotFoundError:
pass
# Import seed data for archive DB (only 'archive' table)
try:
with open("src/lib/db/imports/archive.csv", "r") as f:
from playhouse.sqlite_ext import JSONField as _J
fields = list(Archive._meta.fields)
log.info(f"importing {Archive._meta.table_name} (archive DB)")
reader = csv.DictReader(f)
count = 0
with db_archive.atomic():
for row in reader:
obj = {}
for field in fields:
if type(Archive._meta.fields[field]) is JSONField:
obj[field] = json.loads(row[field])
else:
try:
obj[field] = ast.literal_eval(row[field])
except (SyntaxError, ValueError):
obj[field] = row[field]
Archive.insert(**obj).on_conflict_replace().execute()
count += 1
log.info(f"{Archive._meta.table_name}: imported {count} rows into archive DB.")
except FileNotFoundError:
pass

View File

@ -1,5 +1,5 @@
from .archive import Archive
from .base_model import db
from .base_model import db, db_archive
from .log import Log
from .recipes import Recipes
from .users import Session, Users

View File

@ -1,17 +1,18 @@
from datetime import datetime
from peewee import (AutoField, BooleanField, DateTimeField, ForeignKeyField,
from peewee import (AutoField, BooleanField, DateTimeField,
TextField, IntegerField)
from playhouse.sqlite_ext import JSONField
from .base_model import BaseModel, db
from .base_model import BaseModel, db_archive
from .users import Users
class Archive(BaseModel):
id = AutoField(primary_key=True, unique=True, null=False)
time = DateTimeField(unique=True, null=False, default=datetime.now())
user = ForeignKeyField(Users, Users.username, null=False)
time = DateTimeField(unique=True, null=False, default=datetime.now)
# Store username directly to avoid cross-database foreign keys
user = TextField(null=False)
result = BooleanField(null=False)
overridden = BooleanField(null=False)
test_data = JSONField(null=False)
@ -21,14 +22,14 @@ class Archive(BaseModel):
uploaded = BooleanField(null=False, default=False)
@classmethod
@db.atomic()
@db_archive.atomic()
def archive(cls, test_data, result, overridden):
time=datetime.now()
test_data["time"]=time.strftime("%d/%m/%Y %H:%M:%S")
test_data["user"]=Users.get_session().username
time = datetime.now()
test_data["time"] = time.strftime("%d/%m/%Y %H:%M:%S")
test_data["user"] = Users.get_session().username
return cls.create(
time=time,
user=Users.get_session().user,
user=Users.get_session().username,
result=result,
overridden=overridden,
test_data=test_data,
@ -36,3 +37,4 @@ class Archive(BaseModel):
class Meta:
table_name = "archive"
database = db_archive

View File

@ -1,12 +1,203 @@
import os
import sys
import shutil
import traceback
from peewee import Model
from playhouse.sqlite_ext import SqliteExtDatabase
# GUI popup support (create minimal app if needed)
try:
from PyQt5.QtWidgets import QApplication, QMessageBox
except Exception:
QApplication = None
QMessageBox = None
db_path = "./data/database"
os.makedirs(db_path, exist_ok=True)
db = SqliteExtDatabase(
def _delete_archive_and_folder_and_exit():
# Try to detach any attached archive data source alias 'e' if present
try:
if 'db' in globals() and getattr(globals()['db'], 'is_closed', lambda: True)() is False:
try:
globals()['db'].execute_sql('DETACH DATABASE e')
except Exception:
pass
except Exception:
pass
try:
if 'db_archive' in globals() and getattr(globals()['db_archive'], 'is_closed', lambda: True)() is False:
try:
globals()['db_archive'].execute_sql('DETACH DATABASE e')
except Exception:
pass
except Exception:
pass
# Try closing DBs if already created
try:
if 'db' in globals() and getattr(globals()['db'], 'is_closed', lambda: True)() is False:
try:
globals()['db'].close()
except Exception:
pass
except Exception:
pass
try:
if 'db_archive' in globals() and getattr(globals()['db_archive'], 'is_closed', lambda: True)() is False:
try:
globals()['db_archive'].close()
except Exception:
pass
except Exception:
pass
# Remove archive DB explicitly (base file and side-car files)
try:
base = os.path.join(db_path, 'sqlite_archive.db')
candidates = [
base,
os.path.join(db_path, 'sqlite_archive'),
base + '-wal',
base + '-shm',
base + '-journal',
]
for p in candidates:
try:
os.remove(p)
except Exception:
pass
except Exception:
pass
# Try to recreate the new archive DB and import seed data
try:
# Reconnect will create the file if missing
if 'db_archive' in globals():
try:
if getattr(globals()['db_archive'], 'is_closed', lambda: True)():
globals()['db_archive'].connect()
except Exception:
# If connect fails, try reopen after ensuring close
try:
globals()['db_archive'].close()
except Exception:
pass
try:
globals()['db_archive'].connect()
except Exception:
pass
# Lazy import to avoid circular
try:
from .archive import Archive # type: ignore
# Create table
try:
globals()['db_archive'].create_tables([Archive])
except Exception:
pass
# Import from CSV if present
try:
import csv, json, ast
from playhouse.sqlite_ext import JSONField as _JSONField
path = "src/lib/db/imports/archive.csv"
if os.path.exists(path):
with open(path, "r") as f:
reader = csv.DictReader(f)
fields = list(Archive._meta.fields)
count = 0
with globals()['db_archive'].atomic():
for row in reader:
obj = {}
for field in fields:
if type(Archive._meta.fields[field]) is _JSONField:
obj[field] = json.loads(row.get(field, "null"))
else:
try:
obj[field] = ast.literal_eval(row.get(field, ""))
except (SyntaxError, ValueError):
obj[field] = row.get(field)
try:
Archive.insert(**obj).on_conflict_replace().execute()
count += 1
except Exception:
pass
except Exception:
pass
except Exception:
pass
except Exception:
pass
# Hard exit to ensure restart
try:
os._exit(1)
except Exception:
sys.exit(1)
def handle_fatal_sqlite_error(exc=None):
"""Show popup, print error to terminal, delete only the archive DB, try to re-import, then exit."""
msg_text = "ATTENZIONE ERRORE FATALE DATABASE DI TEST PEFOVARORE PREMERE OK E RIAVVIAREIL PROGRAMMA"
# Always print error details to the execution terminal
try:
sys.stderr.write("FATAL SQLITE ERROR DETECTED\n")
if exc is not None:
sys.stderr.write(f"Exception: {exc!r}\n")
try:
traceback.print_exc()
except Exception:
pass
sys.stderr.flush()
except Exception:
pass
# If GUI available, show a critical popup. Create a temp app if needed.
app_created = False
app = None
try:
if QApplication is not None:
app = QApplication.instance()
if app is None:
app = QApplication([])
app_created = True
if QMessageBox is not None:
QMessageBox.critical(None, "Errore Database", msg_text)
except Exception:
# Also echo the user-facing message to stderr in case GUI cannot be shown
try:
sys.stderr.write(msg_text + "\n")
sys.stderr.flush()
except Exception:
pass
finally:
# Ensure resources are cleaned up and exit (with attempted rebuild)
_delete_archive_and_folder_and_exit()
class SafeSqliteExtDatabase(SqliteExtDatabase):
def execute_sql(self, sql, params=None, commit=True):
try:
return super().execute_sql(sql, params=params, commit=commit)
except Exception as e:
msg = str(e).lower()
if any(s in msg for s in (
"malformed",
"not a database",
"file is encrypted or is not a database",
"database disk image is malformed",
)):
handle_fatal_sqlite_error(e)
raise
# Main application database
db = SafeSqliteExtDatabase(
db_path + "/sqlite.db",
pragmas={ # see https://www.sqlite.org/pragma.html
"auto_vacuum": 1,
@ -20,6 +211,21 @@ db = SqliteExtDatabase(
timeout=5
)
# Separate database dedicated to archive data
db_archive = SafeSqliteExtDatabase(
db_path + "/sqlite_archive.db",
pragmas={
"auto_vacuum": 1,
"busy_timeout": 5000,
"cache_size": round(-64e3),
"foreign_keys": 0, # no FKs across DBs; archive uses simple fields
"ignore_check_constraints": 0,
"journal_mode": "wal",
"synchronous": 1,
},
timeout=5
)
class BaseModel(Model):
"""A base model that will use our Sqlite database."""

View File

@ -971,8 +971,8 @@ class Test(Widget):
# EXTRA DATA
"SHIFT": str(get_shift(archived.time)),
"STATION": str(self.config.machine_id),
"OPERATOR": str(archived.user.username),
"BADGE_NUM": str(archived.user.badge_number),
"OPERATOR": str(archived.user.username) if hasattr(archived.user, "username") else str(archived.user),
"BADGE_NUM": (str(archived.user.badge_number) if hasattr(archived.user, "badge_number") else str((Users.get_user(archived.user).badge_number if Users.get_user(archived.user) else ""))),
# BARCODE
"BCODE": str(self.step.spec.get("barcode","")),