Attempt to cleanup imports.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 []
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user