Attempt to cleanup imports.

This commit is contained in:
lwark
2025-09-12 10:14:53 -05:00
parent ba4912cab7
commit 11abaafcfc
23 changed files with 322 additions and 323 deletions

View File

@@ -3,11 +3,14 @@ Contains all models for sqlalchemy
"""
from __future__ import annotations
import sys, logging, json, inspect
from datetime import datetime, date
from pprint import pformat
from dateutil.parser import parse
from jinja2 import TemplateNotFound
from jinja2 import TemplateNotFound, Template
from pandas import DataFrame
from pydantic import BaseModel
from sqlalchemy import Column, INTEGER, String, JSON, DATETIME
from sqlalchemy import Column, INTEGER, String, JSON, TIMESTAMP
from sqlalchemy.ext.associationproxy import AssociationProxy
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute, ColumnProperty
from sqlalchemy.ext.declarative import declared_attr
@@ -16,7 +19,7 @@ from sqlalchemy.exc import ArgumentError
from typing import Any, List, ClassVar
from pathlib import Path
from sqlalchemy.orm.relationships import _RelationshipDeclared
from tools import report_result, list_sort_dict
from tools import report_result, list_sort_dict, jinja_template_loading, Report, Result
# NOTE: Load testing environment
if 'pytest' in sys.modules:
@@ -46,7 +49,9 @@ class BaseClass(Base):
except AttributeError:
return f"<{self.__class__.__name__}(Name Unavailable)>"
@classproperty
# @classproperty
@classmethod
@declared_attr
def aliases(cls) -> List[str]:
"""
List of other names this class might be known by.
@@ -56,7 +61,8 @@ class BaseClass(Base):
"""
return [cls.query_alias]
@classproperty
@classmethod
@declared_attr
def query_alias(cls) -> str:
"""
What to query this class as.
@@ -126,7 +132,9 @@ class BaseClass(Base):
super().__init__(*args, **kwargs)
self._misc_info = dict()
@classproperty
# @classproperty
@classmethod
@declared_attr
def jsons(cls) -> List[str]:
"""
Get list of JSON db columns
@@ -139,7 +147,9 @@ class BaseClass(Base):
except AttributeError:
return []
@classproperty
# @classproperty
@classmethod
@declared_attr
def timestamps(cls) -> List[str]:
"""
Get list of TIMESTAMP columns
@@ -169,9 +179,6 @@ class BaseClass(Base):
continue
if not isinstance(item[1].property, ColumnProperty):
continue
# if isinstance(item[1], _RelationshipDeclared):
# if "association" in item[0]:
# continue
if len(item[1].foreign_keys) > 0:
continue
if item[1].type.__class__.__name__ not in ["String"]:
@@ -245,14 +252,15 @@ class BaseClass(Base):
"""
if not objects:
try:
records = [obj.details_dict(**kwargs) for obj in cls.query()]
q = cls.query()
except AttributeError:
records = [obj.details_dict(**kwargs) for obj in cls.query(page_size=0)]
q = cls.query(page_size=0)
else:
try:
records = [obj.to_sub_dict(**kwargs) for obj in objects]
except AttributeError:
records = [{k: v['instance_attr'] for k, v in obj.omnigui_instance_dict.items()} for obj in objects]
q = objects
records = []
for obj in q:
dicto = obj.details_dict(**kwargs)
records.append({key: value for key, value in dicto.items() if key not in dicto['excluded']})
return DataFrame.from_records(records)
@classmethod
@@ -397,7 +405,9 @@ class BaseClass(Base):
pass
return dicto
@classproperty
# @classproperty
@classmethod
@declared_attr
def pydantic_model(cls) -> BaseModel:
"""
Gets the pydantic model corresponding to this object.
@@ -410,10 +420,15 @@ class BaseClass(Base):
model = getattr(pydant, f"Pyd{cls.__name__}")
except AttributeError:
logger.warning(f"Couldn't get {cls.__name__} pydantic model.")
return pydant.PydElastic
try:
model = getattr(pydant, f"Pyd{cls.pyd_model_name}")
except AttributeError:
return pydant.PydElastic
return model
@classproperty
# @classproperty
@classmethod
@declared_attr
def add_edit_tooltips(cls) -> dict:
"""
Gets tooltips for Omni-add-edit
@@ -423,7 +438,9 @@ class BaseClass(Base):
"""
return dict()
@classproperty
# @classproperty
@classmethod
@declared_attr
def details_template(cls) -> Template:
"""
Get the details jinja template for the correct class
@@ -609,6 +626,11 @@ class BaseClass(Base):
value = getattr(self, k)
except AttributeError:
continue
match value:
case str():
value = value.strip('\"')
case _:
pass
output[k.strip("_")] = value
if self._misc_info:
for key, value in self._misc_info.items():
@@ -718,12 +740,20 @@ class ConfigItem(BaseClass):
return config_items
from .controls import *
# NOTE: import order must go: orgs, kittype, run due to circular import issues
from .organizations import *
from .procedures import *
from .submissions import *
from .audit import AuditLog
from .organizations import (
ClientLab, Contact, BaseClass # NOTE: For some reason I need to import BaseClass at this point for queries to work.
)
from .procedures import (
ReagentRole, Reagent, ReagentLot, Discount, SubmissionType, ProcedureType, Procedure, ProcedureTypeReagentRoleAssociation,
ProcedureReagentLotAssociation, EquipmentRole, Equipment, EquipmentRoleEquipmentAssociation, Process, ProcessVersion,
Tips, TipsLot, ProcedureEquipmentAssociation, ProcedureTypeEquipmentRoleAssociation, Results
)
from .submissions import (
ClientSubmission, Run, Sample, ClientSubmissionSampleAssociation, RunSampleAssociation, ProcedureSampleAssociation
)
from .controls import ControlType, Control
# NOTE: Add a creator to the procedure 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

@@ -11,6 +11,7 @@ import logging
logger = logging.getLogger(f"submissions.{__name__}")
# NOTE: Need a seperate base for this.
Base: DeclarativeMeta = declarative_base()
class AuditLog(Base):

View File

@@ -4,8 +4,8 @@ All client organization related models.
from __future__ import annotations
import logging
from sqlalchemy import Column, String, INTEGER, ForeignKey, Table
from sqlalchemy.orm import relationship, Query
from . import Base, BaseClass
from sqlalchemy.orm import relationship, Query, declared_attr
from . import BaseClass
from tools import check_authorization, setup_lookup
from typing import List
@@ -14,7 +14,8 @@ logger = logging.getLogger(f"submissions.{__name__}")
# table containing clientlab/contact relationship
clientlab_contact = Table(
"_clientlab_contact",
Base.metadata,
# Base.metadata,
BaseClass.__base__.metadata,
Column("clientlab_id", INTEGER, ForeignKey("_clientlab.id")),
Column("contact_id", INTEGER, ForeignKey("_contact.id")),
extend_existing=True
@@ -98,7 +99,9 @@ class Contact(BaseClass):
secondary=clientlab_contact) #: relationship to joined clientlab
clientsubmission = relationship("ClientSubmission", back_populates="contact") #: procedure this contact has submitted
@classproperty
# @classproperty
@classmethod
@declared_attr
def searchables(cls):
return []

View File

@@ -7,13 +7,13 @@ from operator import itemgetter
from pprint import pformat
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, func
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.orm import relationship, validates, Query, declared_attr
from sqlalchemy.ext.associationproxy import association_proxy
from datetime import date, datetime, timedelta
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \
jinja_template_loading, flatten_list
from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING
from . import Base, BaseClass, ClientLab, LogMixin
from . import BaseClass, ClientLab, LogMixin
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
@@ -25,7 +25,7 @@ logger = logging.getLogger(f'submissions.{__name__}')
reagentrole_reagent = Table(
"_reagentrole_reagent",
Base.metadata,
BaseClass.__base__.metadata,
Column("reagent_id", INTEGER, ForeignKey("_reagent.id")),
Column("reagentrole_id", INTEGER, ForeignKey("_reagentrole.id")),
extend_existing=True
@@ -33,7 +33,7 @@ reagentrole_reagent = Table(
equipment_process = Table(
"_equipment_process",
Base.metadata,
BaseClass.__base__.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("equipment_id", INTEGER, ForeignKey("_equipment.id")),
extend_existing=True
@@ -41,7 +41,7 @@ equipment_process = Table(
process_tips = Table(
"_process_tips",
Base.metadata,
BaseClass.__base__.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("tips_id", INTEGER, ForeignKey("_tips.id")),
extend_existing=True
@@ -49,7 +49,7 @@ process_tips = Table(
submissiontype_proceduretype = Table(
"_submissiontype_proceduretype",
Base.metadata,
BaseClass.__base__.metadata,
Column("submissiontype_id", INTEGER, ForeignKey("_submissiontype.id")),
Column("proceduretype_id", INTEGER, ForeignKey("_proceduretype.id")),
extend_existing=True
@@ -217,7 +217,8 @@ class Reagent(BaseClass, LogMixin):
self.name = name
self.eol_ext = eol_ext
@classproperty
@classmethod
@declared_attr
def searchables(cls):
return [dict(label="Lot", field="lot")]
@@ -322,7 +323,8 @@ class Reagent(BaseClass, LogMixin):
@classproperty
@classmethod
@declared_attr
def add_edit_tooltips(self):
return dict(
expiry="Use exact date on reagent.\nEOL will be calculated from kittype automatically"
@@ -342,6 +344,9 @@ class Reagent(BaseClass, LogMixin):
class ReagentLot(BaseClass):
pyd_model_name = "Reagent"
id = Column(INTEGER, primary_key=True) #: primary key
lot = Column(String(64), unique=True) #: lot number of reagent
expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
@@ -368,7 +373,7 @@ class ReagentLot(BaseClass):
def query(cls,
lot: str | None = None,
name: str | None = None,
limit: int = 1,
limit: int = 0,
**kwargs) -> ReagentLot | List[ReagentLot]:
"""
@@ -386,6 +391,7 @@ class ReagentLot(BaseClass):
match lot:
case str():
query = query.filter(cls.lot == lot)
limit = 1
case _:
pass
match name:
@@ -414,13 +420,28 @@ class ReagentLot(BaseClass):
@check_authorization
def edit_from_search(self, obj, **kwargs):
from frontend.widgets.omni_add_edit import AddEdit
from backend.validators.pydant import PydElastic
dlg = AddEdit(parent=None, instance=self, disabled=['reagent'])
if dlg.exec():
pyd = dlg.parse_form()
for field in pyd.model_fields:
self.set_attribute(field, pyd.__getattribute__(field))
logger.debug(f"Pydantic returned: {type(pyd)} {pyd.model_fields}")
fields = pyd.model_fields
if isinstance(pyd, PydElastic):
fields.update(pyd.model_extra)
for field in fields:
if field in ['instance']:
continue
field_value = pyd.__getattribute__(field)
logger.debug(f"Setting {field} in Reagent Lot to {field_value}")
self.set_attribute(field, field_value)
self.save()
def details_dict(self, **kwargs) -> dict:
output = super().details_dict(**kwargs)
output['excluded'] += ["reagentlotprocedureassociation", "procedures"]
output['reagent'] = output['reagent'].name
return output
class Discount(BaseClass):
"""
Relationship table for client labs for certain kits.
@@ -508,7 +529,8 @@ class SubmissionType(BaseClass):
"""
return f"<SubmissionType({self.name})>"
@classproperty
@classmethod
@declared_attr
def aliases(cls) -> List[str]:
"""
Gets other names the sql object of this class might go by.
@@ -604,12 +626,14 @@ class SubmissionType(BaseClass):
sample_map=self.sample_map
)
@classproperty
@classmethod
@declared_attr
def info_map_json_edit_fields(cls):
dicto = dict()
return dicto
@classproperty
@classmethod
@declared_attr
def regex(cls) -> re.Pattern:
"""
Constructs catchall regex.
@@ -1182,7 +1206,8 @@ class ProcedureTypeReagentRoleAssociation(BaseClass):
dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr'])
return dicto
@classproperty
@classmethod
@declared_attr
def json_edit_fields(cls) -> dict:
dicto = dict(
sheet="str",
@@ -1607,7 +1632,8 @@ class Equipment(BaseClass, LogMixin):
creation_dict['equipmentrole'] = equipmentrole or creation_dict['equipmentrole']
return PydEquipment(**creation_dict)
@classproperty
@classmethod
@declared_attr
def manufacturer_regex(cls) -> re.Pattern:
"""
Creates regex to determine tip manufacturer

View File

@@ -11,9 +11,9 @@ from pprint import pformat
from pandas import DataFrame
from sqlalchemy.ext.hybrid import hybrid_property
from frontend.widgets.functions import select_save_file
from . import Base, BaseClass, SubmissionType, ClientLab, Contact, LogMixin, Procedure
from . import BaseClass, SubmissionType, ClientLab, Contact, LogMixin, Procedure
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func
from sqlalchemy.orm import relationship, Query
from sqlalchemy.orm import relationship, Query, declared_attr
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
@@ -947,7 +947,8 @@ class Run(BaseClass, LogMixin):
# NOTE: Polymorphic functions
@classproperty
@classmethod
@declared_attr
def regex(cls) -> re.Pattern:
"""
Constructs catchall regex.
@@ -1297,7 +1298,8 @@ class Sample(BaseClass, LogMixin):
def __repr__(self) -> str:
return f"<Sample({self.sample_id})>"
@classproperty
@classmethod
@declared_attr
def searchables(cls):
return [dict(label="Submitter ID", field="sample_id")]