Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fdd0dd977 | ||
|
|
9d5c935fd2 |
|
|
@ -223,7 +223,7 @@ class ArchiveSynchronizer(Component):
|
||||||
"result": "OK" if record.result else "KO",
|
"result": "OK" if record.result else "KO",
|
||||||
"serial": record.id,
|
"serial": record.id,
|
||||||
"time": record.time.isoformat(),
|
"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",
|
"barcode_out": record.barcode if record.barcode else "NA",
|
||||||
}, timeout=5, verify=False)
|
}, timeout=5, verify=False)
|
||||||
else:
|
else:
|
||||||
|
|
@ -234,7 +234,7 @@ class ArchiveSynchronizer(Component):
|
||||||
"result": "OK" if record.result else "KO",
|
"result": "OK" if record.result else "KO",
|
||||||
"serial": record.id,
|
"serial": record.id,
|
||||||
"time": record.time.isoformat(),
|
"time": record.time.isoformat(),
|
||||||
"user": record.user.username,
|
"user": (record.user.username if hasattr(record.user, "username") else record.user),
|
||||||
}, timeout=5, verify=False)
|
}, timeout=5, verify=False)
|
||||||
|
|
||||||
if r.status_code != 200:
|
if r.status_code != 200:
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@ import csv
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from playhouse.sqlite_ext import JSONField
|
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 = {
|
models_reference = {
|
||||||
"archive": Archive,
|
"archive": Archive,
|
||||||
"log": Log,
|
"log": Log,
|
||||||
|
|
@ -15,39 +19,97 @@ models_reference = {
|
||||||
"users": Users,
|
"users": Users,
|
||||||
}
|
}
|
||||||
|
|
||||||
db.connect()
|
# Optional test flag to simulate on-disk SQLite corruption (archive DB only)
|
||||||
db.create_tables(list(models_reference.values()))
|
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")
|
log = logging.getLogger("db")
|
||||||
|
|
||||||
@db.atomic()
|
@db.atomic()
|
||||||
def init_db():
|
def init_db():
|
||||||
|
# Import seed data for main DB
|
||||||
tables = db.get_tables()
|
tables = db.get_tables()
|
||||||
tables.sort()
|
tables.sort()
|
||||||
for table in tables:
|
for table in tables:
|
||||||
count = 0
|
count = 0
|
||||||
try:
|
try:
|
||||||
with open(f"src/lib/db/imports/{table}.csv", "r") as f:
|
with open(f"src/lib/db/imports/{table}.csv", "r") as f:
|
||||||
table = models_reference[table]
|
table_model = models_reference[table]
|
||||||
fields = list(table._meta.fields)
|
fields = list(table_model._meta.fields)
|
||||||
log.info(f"importing {table._meta.table_name}")
|
log.info(f"importing {table_model._meta.table_name}")
|
||||||
reader = csv.DictReader(f)
|
reader = csv.DictReader(f)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
obj = {}
|
obj = {}
|
||||||
for field in fields:
|
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])
|
obj[field] = json.loads(row[field])
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
obj[field] = ast.literal_eval(row[field])
|
obj[field] = ast.literal_eval(row[field])
|
||||||
except (SyntaxError, ValueError):
|
except (SyntaxError, ValueError):
|
||||||
obj[field] = row[field]
|
obj[field] = row[field]
|
||||||
table.insert(**obj).on_conflict_replace().execute()
|
table_model.insert(**obj).on_conflict_replace().execute()
|
||||||
count += 1
|
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:
|
except FileNotFoundError:
|
||||||
pass
|
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
|
||||||
|
|
||||||
|
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from .archive import Archive
|
from .archive import Archive
|
||||||
from .base_model import db
|
from .base_model import db, db_archive
|
||||||
from .log import Log
|
from .log import Log
|
||||||
from .recipes import Recipes
|
from .recipes import Recipes
|
||||||
from .users import Session, Users
|
from .users import Session, Users
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from peewee import (AutoField, BooleanField, DateTimeField, ForeignKeyField,
|
from peewee import (AutoField, BooleanField, DateTimeField,
|
||||||
TextField, IntegerField)
|
TextField, IntegerField)
|
||||||
from playhouse.sqlite_ext import JSONField
|
from playhouse.sqlite_ext import JSONField
|
||||||
|
|
||||||
from .base_model import BaseModel, db
|
from .base_model import BaseModel, db_archive
|
||||||
from .users import Users
|
from .users import Users
|
||||||
|
|
||||||
|
|
||||||
class Archive(BaseModel):
|
class Archive(BaseModel):
|
||||||
id = AutoField(primary_key=True, unique=True, null=False)
|
id = AutoField(primary_key=True, unique=True, null=False)
|
||||||
time = DateTimeField(unique=True, null=False, default=datetime.now())
|
time = DateTimeField(unique=True, null=False, default=datetime.now)
|
||||||
user = ForeignKeyField(Users, Users.username, null=False)
|
# Store username directly to avoid cross-database foreign keys
|
||||||
|
user = TextField(null=False)
|
||||||
result = BooleanField(null=False)
|
result = BooleanField(null=False)
|
||||||
overridden = BooleanField(null=False)
|
overridden = BooleanField(null=False)
|
||||||
test_data = JSONField(null=False)
|
test_data = JSONField(null=False)
|
||||||
|
|
@ -21,14 +22,14 @@ class Archive(BaseModel):
|
||||||
uploaded = BooleanField(null=False, default=False)
|
uploaded = BooleanField(null=False, default=False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@db.atomic()
|
@db_archive.atomic()
|
||||||
def archive(cls, test_data, result, overridden):
|
def archive(cls, test_data, result, overridden):
|
||||||
time=datetime.now()
|
time = datetime.now()
|
||||||
test_data["time"]=time.strftime("%d/%m/%Y %H:%M:%S")
|
test_data["time"] = time.strftime("%d/%m/%Y %H:%M:%S")
|
||||||
test_data["user"]=Users.get_session().username
|
test_data["user"] = Users.get_session().username
|
||||||
return cls.create(
|
return cls.create(
|
||||||
time=time,
|
time=time,
|
||||||
user=Users.get_session().user,
|
user=Users.get_session().username,
|
||||||
result=result,
|
result=result,
|
||||||
overridden=overridden,
|
overridden=overridden,
|
||||||
test_data=test_data,
|
test_data=test_data,
|
||||||
|
|
@ -36,3 +37,4 @@ class Archive(BaseModel):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "archive"
|
table_name = "archive"
|
||||||
|
database = db_archive
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,203 @@
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
import traceback
|
||||||
|
|
||||||
from peewee import Model
|
from peewee import Model
|
||||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
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"
|
db_path = "./data/database"
|
||||||
os.makedirs(db_path, exist_ok=True)
|
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",
|
db_path + "/sqlite.db",
|
||||||
pragmas={ # see https://www.sqlite.org/pragma.html
|
pragmas={ # see https://www.sqlite.org/pragma.html
|
||||||
"auto_vacuum": 1,
|
"auto_vacuum": 1,
|
||||||
|
|
@ -20,6 +211,21 @@ db = SqliteExtDatabase(
|
||||||
timeout=5
|
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):
|
class BaseModel(Model):
|
||||||
"""A base model that will use our Sqlite database."""
|
"""A base model that will use our Sqlite database."""
|
||||||
|
|
|
||||||
|
|
@ -971,8 +971,8 @@ class Test(Widget):
|
||||||
# EXTRA DATA
|
# EXTRA DATA
|
||||||
"SHIFT": str(get_shift(archived.time)),
|
"SHIFT": str(get_shift(archived.time)),
|
||||||
"STATION": str(self.config.machine_id),
|
"STATION": str(self.config.machine_id),
|
||||||
"OPERATOR": str(archived.user.username),
|
"OPERATOR": str(archived.user.username) if hasattr(archived.user, "username") else str(archived.user),
|
||||||
"BADGE_NUM": str(archived.user.badge_number),
|
"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
|
# BARCODE
|
||||||
"BCODE": str(self.step.spec.get("barcode","")),
|
"BCODE": str(self.step.spec.get("barcode","")),
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user