Mid-progress adding controls to pydantic creation.

This commit is contained in:
lwark
2024-11-21 08:46:41 -06:00
parent 506aac80c1
commit 7d1e6dc606
16 changed files with 224 additions and 187 deletions

View File

@@ -1,8 +1,10 @@
"""
All database related operations.
"""
from sqlalchemy import event
import sqlalchemy.orm
from sqlalchemy import event, inspect
from sqlalchemy.engine import Engine
from tools import ctx
@@ -16,7 +18,7 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
Args:
dbapi_connection (_type_): _description_
connection_record (_type_): _description_
"""
"""
cursor = dbapi_connection.cursor()
# print(ctx.database_schema)
if ctx.database_schema == "sqlite":
@@ -34,3 +36,39 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
from .models import *
def update_log(mapper, connection, target):
logger.debug("\n\nBefore update\n\n")
state = inspect(target)
logger.debug(state)
update = dict(user=getuser(), time=datetime.now(), object=str(state.object), changes=[])
logger.debug(update)
for attr in state.attrs:
hist = attr.load_history()
if not hist.has_changes():
continue
added = [str(item) for item in hist.added]
deleted = [str(item) for item in hist.deleted]
change = dict(field=attr.key, added=added, deleted=deleted)
logger.debug(f"Adding: {pformat(change)}")
try:
update['changes'].append(change)
except Exception as e:
logger.error(f"Something went horribly wrong adding attr: {attr.key}: {e}")
continue
logger.debug(f"Adding to audit logs: {pformat(update)}")
if update['changes']:
# Note: must use execute as the session will be busy at this point.
# https://medium.com/@singh.surbhicse/creating-audit-table-to-log-insert-update-and-delete-changes-in-flask-sqlalchemy-f2ca53f7b02f
table = AuditLog.__table__
logger.debug(f"Adding to {table}")
connection.execute(table.insert().values(**update))
# logger.debug("Here is where I would insert values, if I was able.")
else:
logger.info(f"No changes detected, not updating logs.")
# event.listen(LogMixin, 'after_update', update_log, propagate=True)
# event.listen(LogMixin, 'after_insert', update_log, propagate=True)

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
import sys, logging
import pandas as pd
from sqlalchemy import Column, INTEGER, String, JSON
from sqlalchemy import Column, INTEGER, String, JSON, event, inspect
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.exc import ArgumentError
@@ -22,6 +22,12 @@ Base: DeclarativeMeta = declarative_base()
logger = logging.getLogger(f"submissions.{__name__}")
class LogMixin(Base):
__abstract__ = True
class BaseClass(Base):
"""
Abstract class to pass ctx values to all SQLAlchemy objects.
@@ -99,6 +105,15 @@ class BaseClass(Base):
singles = list(set(cls.singles + BaseClass.singles))
return dict(singles=singles)
@classmethod
def find_regular_subclass(cls, name: str | None = None):
if not name:
return cls
if " " in name:
search = name.title().replace(" ", "")
logger.debug(f"Searching for subclass: {search}")
return next((item for item in cls.__subclasses__() if item.__name__ == search), cls)
@classmethod
def fuzzy_search(cls, **kwargs):
query: Query = cls.__database_session__.query(cls)
@@ -175,9 +190,11 @@ class BaseClass(Base):
"""
# logger.debug(f"Saving object: {pformat(self.__dict__)}")
report = Report()
state = inspect(self)
try:
self.__database_session__.add(self)
self.__database_session__.commit()
return state
except Exception as e:
logger.critical(f"Problem saving object: {e}")
logger.error(f"Error message: {type(e)}")
@@ -186,6 +203,8 @@ class BaseClass(Base):
return report
class ConfigItem(BaseClass):
"""
Key:JSON objects to store config settings in database.
@@ -222,6 +241,7 @@ from .controls import *
from .organizations import *
from .kits import *
from .submissions import *
from .audit import AuditLog
# NOTE: Add a creator to the submission for reagent association. Assigned here due to circular import constraints.
# https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator

View File

@@ -16,6 +16,7 @@ from typing import List, Literal, Tuple, Generator
from dateutil.parser import parse
from re import Pattern
logger = logging.getLogger(f"submissions.{__name__}")
@@ -429,6 +430,10 @@ class PCRControl(Control):
fig = PCRFigure(df=df, modes=[])
return report, fig
def to_pydantic(self):
from backend.validators import PydPCRControl
return PydPCRControl(**self.to_sub_dict())
class IridaControl(Control):
@@ -878,3 +883,7 @@ class IridaControl(Control):
exclude = [re.sub(rerun_regex, "", sample) for sample in sample_names if rerun_regex.search(sample)]
df = df[df.name not in exclude]
return df
def to_pydantic(self):
from backend.validators import PydIridaControl
return PydIridaControl(**self.__dict__)

View File

@@ -567,7 +567,7 @@ class Reagent(BaseClass):
return cls.execute_query(query=query, limit=limit)
@check_authorization
def edit_from_search(self, **kwargs):
def edit_from_search(self, obj, **kwargs):
from frontend.widgets.misc import AddReagentForm
role = ReagentRole.query(kwargs['role'])
if role:
@@ -1279,7 +1279,10 @@ class SubmissionReagentAssociation(BaseClass):
Returns:
str: Representation of this SubmissionReagentAssociation
"""
return f"<{self.submission.rsl_plate_num} & {self.reagent.lot}>"
try:
return f"<{self.submission.rsl_plate_num} & {self.reagent.lot}>"
except AttributeError:
return f"<Unknown Submission & {self.reagent.lot}"
def __init__(self, reagent=None, submission=None):
if isinstance(reagent, list):

View File

@@ -12,8 +12,8 @@ from zipfile import ZipFile
from tempfile import TemporaryDirectory, TemporaryFile
from operator import itemgetter
from pprint import pformat
from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case
from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, LogMixin
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, event, inspect
from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.ext.associationproxy import association_proxy
@@ -36,7 +36,7 @@ from PIL import Image
logger = logging.getLogger(f"submissions.{__name__}")
class BasicSubmission(BaseClass):
class BasicSubmission(BaseClass, LogMixin):
"""
Concrete of basic submission which polymorphs into BacterialCulture and Wastewater
"""
@@ -544,7 +544,7 @@ class BasicSubmission(BaseClass):
field_value = len(self.samples)
else:
field_value = value
case "ctx" | "csv" | "filepath" | "equipment":
case "ctx" | "csv" | "filepath" | "equipment" | "controls":
return
case item if item in self.jsons():
match key:
@@ -577,10 +577,13 @@ class BasicSubmission(BaseClass):
except AttributeError:
field_value = value
# NOTE: insert into field
try:
self.__setattr__(key, field_value)
except AttributeError as e:
logger.error(f"Could not set {self} attribute {key} to {value} due to \n{e}")
current = self.__getattribute__(key)
if field_value and current != field_value:
logger.debug(f"Updated value: {key}: {current} to {field_value}")
try:
self.__setattr__(key, field_value)
except AttributeError as e:
logger.error(f"Could not set {self} attribute {key} to {value} due to \n{e}")
def update_subsampassoc(self, sample: BasicSample, input_dict: dict):
"""
@@ -1339,7 +1342,7 @@ class BasicSubmission(BaseClass):
# Below are the custom submission types
class BacterialCulture(BasicSubmission):
class BacterialCulture(BasicSubmission, LogMixin):
"""
derivative submission type from BasicSubmission
"""
@@ -1426,7 +1429,7 @@ class BacterialCulture(BasicSubmission):
return input_dict
class Wastewater(BasicSubmission):
class Wastewater(BasicSubmission, LogMixin):
"""
derivative submission type from BasicSubmission
"""
@@ -2189,6 +2192,8 @@ class BasicSample(BaseClass):
Base of basic sample which polymorphs into BCSample and WWSample
"""
searchables = ['submitter_id']
id = Column(INTEGER, primary_key=True) #: primary key
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
sample_type = Column(String(32)) #: mode_sub_type of sample
@@ -2509,6 +2514,9 @@ class BasicSample(BaseClass):
if dlg.exec():
pass
def edit_from_search(self, obj, **kwargs):
self.show_details(obj)
# Below are the custom sample types
@@ -2516,6 +2524,9 @@ class WastewaterSample(BasicSample):
"""
Derivative wastewater sample
"""
searchables = BasicSample.searchables + ['ww_processing_num', 'ww_full_sample_id', 'rsl_number']
id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True)
ww_processing_num = Column(String(64)) #: wastewater processing number
ww_full_sample_id = Column(String(64)) #: full id given by entrics