Middle of the road.

This commit is contained in:
lwark
2025-01-29 14:12:38 -06:00
parent 73752cde87
commit 3fed8daade
11 changed files with 495 additions and 188 deletions

View File

@@ -8,7 +8,7 @@ from pydantic import BaseModel
from sqlalchemy import Column, INTEGER, String, JSON from sqlalchemy import Column, INTEGER, String, JSON
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.exc import ArgumentError from sqlalchemy.exc import ArgumentError, InvalidRequestError
from typing import Any, List from typing import Any, List
from pathlib import Path from pathlib import Path
from tools import report_result, list_sort_dict from tools import report_result, list_sort_dict
@@ -48,7 +48,7 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: Will only add new columns __table_args__ = {'extend_existing': True} #: Will only add new columns
singles = ['id'] singles = ['id']
omni_removes = ['submissions'] omni_removes = ['submissions', "omnigui_class_dict", "omnigui_instance_dict"]
omni_sort = ["name"] omni_sort = ["name"]
@classproperty @classproperty
@@ -60,7 +60,7 @@ class BaseClass(Base):
@classproperty @classproperty
def aliases(cls): def aliases(cls):
return [cls.__name__.lower()] return [cls.query_alias]
@classproperty @classproperty
def level(cls): def level(cls):
@@ -198,7 +198,7 @@ class BaseClass(Base):
try: try:
records = [obj.to_sub_dict(**kwargs) for obj in objects] records = [obj.to_sub_dict(**kwargs) for obj in objects]
except AttributeError: except AttributeError:
records = [{k:v['instance_attr'] for k, v in obj.to_omnigui_dict(**kwargs).items()} for obj in objects] records = [{k:v['instance_attr'] for k, v in obj.omnigui_instance_dict.items()} for obj in objects]
return DataFrame.from_records(records) return DataFrame.from_records(records)
@classmethod @classmethod
@@ -233,6 +233,10 @@ class BaseClass(Base):
logger.info(f"Using key: {k} with value: {v}") logger.info(f"Using key: {k} with value: {v}")
try: try:
attr = getattr(model, k) attr = getattr(model, k)
# NOTE: account for attrs that use list.
if attr.property.uselist:
query = query.filter(attr.contains(v))
else:
query = query.filter(attr == v) query = query.filter(attr == v)
except (ArgumentError, AttributeError) as e: except (ArgumentError, AttributeError) as e:
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.") logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
@@ -265,14 +269,13 @@ class BaseClass(Base):
return report return report
@property @property
def omnigui_dict(self) -> dict: def omnigui_instance_dict(self) -> dict:
""" """
For getting any object in an omni-thing friendly output. For getting any object in an omni-thing friendly output.
Returns: Returns:
dict: Dictionary of object minus _sa_instance_state with id at the front. dict: Dictionary of object minus _sa_instance_state with id at the front.
""" """
# dicto = {k: v for k, v in self.__dict__.items() if k not in ["_sa_instance_state"]}
dicto = {key: dict(class_attr=getattr(self.__class__, key), instance_attr=getattr(self, key)) dicto = {key: dict(class_attr=getattr(self.__class__, key), instance_attr=getattr(self, key))
for key in dir(self.__class__) if for key in dir(self.__class__) if
isinstance(getattr(self.__class__, key), InstrumentedAttribute) and key not in self.omni_removes isinstance(getattr(self.__class__, key), InstrumentedAttribute) and key not in self.omni_removes
@@ -306,7 +309,7 @@ class BaseClass(Base):
model = getattr(pydant, f"Pyd{cls.__name__}") model = getattr(pydant, f"Pyd{cls.__name__}")
except AttributeError: except AttributeError:
logger.warning(f"Couldn't get {cls.__name__} pydantic model.") logger.warning(f"Couldn't get {cls.__name__} pydantic model.")
return None return pydant.PydElastic
return model return model
@classproperty @classproperty
@@ -324,6 +327,32 @@ class BaseClass(Base):
query_kwargs = {relationship_instance.query_alias:relationship_instance} query_kwargs = {relationship_instance.query_alias:relationship_instance}
return cls.query(**query_kwargs) return cls.query(**query_kwargs)
def __setattr__(self, key, value):
try:
field_type = getattr(self.__class__, key)
except AttributeError:
return super().__setattr__(key, value)
try:
check = field_type.property.uselist
except AttributeError:
check = False
if check:
logger.debug(f"Setting with uselist")
if self.__getattribute__(key) is not None:
if isinstance(value, list):
value = self.__getattribute__(key) + value
else:
value = self.__getattribute__(key) + [value]
else:
value = [value]
else:
if isinstance(value, list):
if len(value) == 1:
value = value[0]
else:
raise ValueError("Object is too long to parse a single value.")
return super().__setattr__(key, value)

View File

@@ -146,7 +146,7 @@ class Control(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
submission_type: str | None = None, submissiontype: str | None = None,
subtype: str | None = None, subtype: str | None = None,
start_date: date | str | int | None = None, start_date: date | str | int | None = None,
end_date: date | str | int | None = None, end_date: date | str | int | None = None,
@@ -169,13 +169,13 @@ class Control(BaseClass):
""" """
from backend.db import SubmissionType from backend.db import SubmissionType
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
match submission_type: match submissiontype:
case str(): case str():
from backend import BasicSubmission, SubmissionType from backend import BasicSubmission, SubmissionType
query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type) query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submissiontype)
case SubmissionType(): case SubmissionType():
from backend import BasicSubmission from backend import BasicSubmission
query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submission_type.name) query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submissiontype.name)
case _: case _:
pass pass
# NOTE: by control type # NOTE: by control type
@@ -347,7 +347,7 @@ class PCRControl(Control):
parent.mode_typer.clear() parent.mode_typer.clear()
parent.mode_typer.setEnabled(False) parent.mode_typer.setEnabled(False)
report = Report() report = Report()
controls = cls.query(submission_type=chart_settings['sub_type'], start_date=chart_settings['start_date'], controls = cls.query(submissiontype=chart_settings['sub_type'], start_date=chart_settings['start_date'],
end_date=chart_settings['end_date']) end_date=chart_settings['end_date'])
data = [control.to_sub_dict() for control in controls] data = [control.to_sub_dict() for control in controls]
df = DataFrame.from_records(data) df = DataFrame.from_records(data)

View File

@@ -96,7 +96,8 @@ class KitType(BaseClass):
Base of kits used in submission processing Base of kits used in submission processing
""" """
query_alias = "kit_type" # query_alias = "kit_type"
omni_sort = BaseClass.omni_sort + ["kit_submissiontype_associations", "kit_reagentrole_associations", "processes"]
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True) #: name of kit name = Column(String(64), unique=True) #: name of kit
@@ -134,7 +135,12 @@ class KitType(BaseClass):
@classproperty @classproperty
def aliases(cls): def aliases(cls):
return super().aliases + [cls.query_alias, "kit_types"] return super().aliases + [cls.query_alias, "kit_types", "kit_type"]
@hybrid_property
def submissiontype(self):
"""Alias used_for field to allow query with SubmissionType query alias"""
return self.used_for
def get_reagents(self, def get_reagents(self,
required: bool = False, required: bool = False,
@@ -227,7 +233,8 @@ class KitType(BaseClass):
name: str = None, name: str = None,
used_for: str | SubmissionType | None = None, used_for: str | SubmissionType | None = None,
id: int | None = None, id: int | None = None,
limit: int = 0 limit: int = 0,
**kwargs
) -> KitType | List[KitType]: ) -> KitType | List[KitType]:
""" """
Lookup a list of or single KitType. Lookup a list of or single KitType.
@@ -264,7 +271,7 @@ class KitType(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit, **kwargs)
@check_authorization @check_authorization
def save(self): def save(self):
@@ -407,10 +414,11 @@ class ReagentRole(BaseClass):
@setup_lookup @setup_lookup
def query(cls, def query(cls,
name: str | None = None, name: str | None = None,
kit_type: KitType | str | None = None, kittype: KitType | str | None = None,
reagent: Reagent | str | None = None, reagent: Reagent | str | None = None,
id: int | None = None, id: int | None = None,
limit: int = 0, limit: int = 0,
**kwargs
) -> ReagentRole | List[ReagentRole]: ) -> ReagentRole | List[ReagentRole]:
""" """
Lookup reagent types in the database. Lookup reagent types in the database.
@@ -429,14 +437,14 @@ class ReagentRole(BaseClass):
ReagentRole|List[ReagentRole]: ReagentRole or list of ReagentRoles matching filter. ReagentRole|List[ReagentRole]: ReagentRole or list of ReagentRoles matching filter.
""" """
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
if (kit_type is not None and reagent is None) or (reagent is not None and kit_type is None): if (kittype is not None and reagent is None) or (reagent is not None and kittype is None):
raise ValueError("Cannot filter without both reagent and kit type.") raise ValueError("Cannot filter without both reagent and kit type.")
elif kit_type is None and reagent is None: elif kittype is None and reagent is None:
pass pass
else: else:
match kit_type: match kittype:
case str(): case str():
kit_type = KitType.query(name=kit_type) kittype = KitType.query(name=kittype)
case _: case _:
pass pass
match reagent: match reagent:
@@ -446,7 +454,7 @@ class ReagentRole(BaseClass):
pass pass
assert reagent.role assert reagent.role
# NOTE: Get all roles common to the reagent and the kit. # NOTE: Get all roles common to the reagent and the kit.
result = set(kit_type.reagent_roles).intersection(reagent.role) result = set(kittype.reagent_roles).intersection(reagent.role)
return next((item for item in result), None) return next((item for item in result), None)
match name: match name:
case str(): case str():
@@ -491,8 +499,6 @@ class Reagent(BaseClass, LogMixin):
Concrete reagent instance Concrete reagent instance
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
role = relationship("ReagentRole", back_populates="instances", role = relationship("ReagentRole", back_populates="instances",
secondary=reagentroles_reagents) #: joined parent reagent type secondary=reagentroles_reagents) #: joined parent reagent type
@@ -523,6 +529,11 @@ class Reagent(BaseClass, LogMixin):
def searchables(cls): def searchables(cls):
return [dict(label="Lot", field="lot")] return [dict(label="Lot", field="lot")]
@hybrid_property
def reagentrole(self):
"""Alias role field to allow query with ReagentRole query alias"""
return self.role
def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict: def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict:
""" """
dictionary containing values necessary for gui dictionary containing values necessary for gui
@@ -583,9 +594,9 @@ class Reagent(BaseClass, LogMixin):
Report: Result of operation Report: Result of operation
""" """
report = Report() report = Report()
rt = ReagentRole.query(kit_type=kit, reagent=self, limit=1) rt = ReagentRole.query(kittype=kit, reagent=self, limit=1)
if rt is not None: if rt is not None:
assoc = KitTypeReagentRoleAssociation.query(kit_type=kit, reagent_role=rt) assoc = KitTypeReagentRoleAssociation.query(kittype=kit, reagentrole=rt)
if assoc is not None: if assoc is not None:
if assoc.last_used != self.lot: if assoc.last_used != self.lot:
assoc.last_used = self.lot assoc.last_used = self.lot
@@ -621,7 +632,8 @@ class Reagent(BaseClass, LogMixin):
role: str | ReagentRole | None = None, role: str | ReagentRole | None = None,
lot: str | None = None, lot: str | None = None,
name: str | None = None, name: str | None = None,
limit: int = 0 limit: int = 0,
**kwargs
) -> Reagent | List[Reagent]: ) -> Reagent | List[Reagent]:
""" """
Lookup a list of reagents from the database. Lookup a list of reagents from the database.
@@ -643,6 +655,8 @@ class Reagent(BaseClass, LogMixin):
limit = 1 limit = 1
case _: case _:
pass pass
# if not role and "reagentrole" in kwargs.keys():
# role = kwargs['reagentrole']
match role: match role:
case str(): case str():
query = query.join(cls.role).filter(ReagentRole.name == role) query = query.join(cls.role).filter(ReagentRole.name == role)
@@ -663,7 +677,7 @@ class Reagent(BaseClass, LogMixin):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit, **kwargs)
def set_attribute(self, key, value): def set_attribute(self, key, value):
match key: match key:
@@ -741,7 +755,7 @@ class Discount(BaseClass):
@setup_lookup @setup_lookup
def query(cls, def query(cls,
organization: Organization | str | int | None = None, organization: Organization | str | int | None = None,
kit_type: KitType | str | int | None = None, kittype: KitType | str | int | None = None,
) -> Discount | List[Discount]: ) -> Discount | List[Discount]:
""" """
Lookup discount objects (union of kit and organization) Lookup discount objects (union of kit and organization)
@@ -763,13 +777,13 @@ class Discount(BaseClass):
query = query.join(Organization).filter(Organization.id == organization) query = query.join(Organization).filter(Organization.id == organization)
case _: case _:
pass pass
match kit_type: match kittype:
case KitType(): case KitType():
query = query.filter(cls.kit == kit_type) query = query.filter(cls.kit == kittype)
case str(): case str():
query = query.join(KitType).filter(KitType.name == kit_type) query = query.join(KitType).filter(KitType.name == kittype)
case int(): case int():
query = query.join(KitType).filter(KitType.id == kit_type) query = query.join(KitType).filter(KitType.id == kittype)
case _: case _:
pass pass
return cls.execute_query(query=query) return cls.execute_query(query=query)
@@ -833,6 +847,14 @@ class SubmissionType(BaseClass):
""" """
return f"<SubmissionType({self.name})>" return f"<SubmissionType({self.name})>"
@hybrid_property
def kittype(self):
return self.kit_types
@hybrid_property
def process(self):
return self.processes
@classproperty @classproperty
def aliases(cls): def aliases(cls):
return super().aliases + ["submission_types", "submission_type"] return super().aliases + ["submission_types", "submission_type"]
@@ -990,7 +1012,8 @@ class SubmissionType(BaseClass):
def query(cls, def query(cls,
name: str | None = None, name: str | None = None,
key: str | None = None, key: str | None = None,
limit: int = 0 limit: int = 0,
**kwargs
) -> SubmissionType | List[SubmissionType]: ) -> SubmissionType | List[SubmissionType]:
""" """
Lookup submission type in the database by a number of parameters Lookup submission type in the database by a number of parameters
@@ -1124,7 +1147,23 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
Returns: Returns:
str: Representation of this object str: Representation of this object
""" """
return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name}&{self.kit_type.name})>" try:
submission_type_name = self.submission_type.name
except AttributeError:
submission_type_name = "None"
try:
kit_type_name = self.kit_type.name
except AttributeError:
kit_type_name = "None"
return f"<SubmissionTypeKitTypeAssociation({submission_type_name}&{kit_type_name})>"
@hybrid_property
def kittype(self):
return self.kit_type
@hybrid_property
def submissiontype(self):
return self.submission_type
@property @property
def name(self): def name(self):
@@ -1133,8 +1172,8 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
submission_type: SubmissionType | str | int | None = None, submissiontype: SubmissionType | str | int | None = None,
kit_type: KitType | str | int | None = None, kittype: KitType | str | int | None = None,
limit: int = 0, limit: int = 0,
**kwargs **kwargs
) -> SubmissionTypeKitTypeAssociation | List[SubmissionTypeKitTypeAssociation]: ) -> SubmissionTypeKitTypeAssociation | List[SubmissionTypeKitTypeAssociation]:
@@ -1150,21 +1189,21 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
SubmissionTypeKitTypeAssociation|List[SubmissionTypeKitTypeAssociation]: SubmissionTypeKitTypeAssociation(s) of interest SubmissionTypeKitTypeAssociation|List[SubmissionTypeKitTypeAssociation]: SubmissionTypeKitTypeAssociation(s) of interest
""" """
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
match submission_type: match submissiontype:
case SubmissionType(): case SubmissionType():
query = query.filter(cls.submission_type == submission_type) query = query.filter(cls.submission_type == submissiontype)
case str(): case str():
query = query.join(SubmissionType).filter(SubmissionType.name == submission_type) query = query.join(SubmissionType).filter(SubmissionType.name == submissiontype)
case int(): case int():
query = query.join(SubmissionType).filter(SubmissionType.id == submission_type) query = query.join(SubmissionType).filter(SubmissionType.id == submissiontype)
match kit_type: match kittype:
case KitType(): case KitType():
query = query.filter(cls.kit_type == kit_type) query = query.filter(cls.kit_type == kittype)
case str(): case str():
query = query.join(KitType).filter(KitType.name == kit_type) query = query.join(KitType).filter(KitType.name == kittype)
case int(): case int():
query = query.join(KitType).filter(KitType.id == kit_type) query = query.join(KitType).filter(KitType.id == kittype)
if kit_type is not None and submission_type is not None: if kittype is not None and submissiontype is not None:
limit = 1 limit = 1
# limit = query.count() # limit = query.count()
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@@ -1221,7 +1260,10 @@ class KitTypeReagentRoleAssociation(BaseClass):
@property @property
def name(self): def name(self):
try:
return f"{self.kit_type.name} -> {self.reagent_role.name}" return f"{self.kit_type.name} -> {self.reagent_role.name}"
except AttributeError:
return "Blank KitTypeReagentRole"
@validates('required') @validates('required')
def validate_required(self, key, value): def validate_required(self, key, value):
@@ -1238,6 +1280,8 @@ class KitTypeReagentRoleAssociation(BaseClass):
Returns: Returns:
_type_: value _type_: value
""" """
if isinstance(value, bool):
value = int(value)
if not 0 <= value < 2: if not 0 <= value < 2:
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.') raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
return value return value
@@ -1264,8 +1308,8 @@ class KitTypeReagentRoleAssociation(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
kit_type: KitType | str | None = None, kittype: KitType | str | None = None,
reagent_role: ReagentRole | str | None = None, reagentrole: ReagentRole | str | None = None,
limit: int = 0, limit: int = 0,
**kwargs **kwargs
) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]: ) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]:
@@ -1281,21 +1325,21 @@ class KitTypeReagentRoleAssociation(BaseClass):
models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest. models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest.
""" """
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
match kit_type: match kittype:
case KitType(): case KitType():
query = query.filter(cls.kit_type == kit_type) query = query.filter(cls.kit_type == kittype)
case str(): case str():
query = query.join(KitType).filter(KitType.name == kit_type) query = query.join(KitType).filter(KitType.name == kittype)
case _: case _:
pass pass
match reagent_role: match reagentrole:
case ReagentRole(): case ReagentRole():
query = query.filter(cls.reagent_role == reagent_role) query = query.filter(cls.reagent_role == reagentrole)
case str(): case str():
query = query.join(ReagentRole).filter(ReagentRole.name == reagent_role) query = query.join(ReagentRole).filter(ReagentRole.name == reagentrole)
case _: case _:
pass pass
if kit_type is not None and reagent_role is not None: if kittype is not None and reagentrole is not None:
limit = 1 limit = 1
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@@ -1329,8 +1373,8 @@ class KitTypeReagentRoleAssociation(BaseClass):
yield rel_reagent yield rel_reagent
@property @property
def omnigui_dict(self) -> dict: def omnigui_instance_dict(self) -> dict:
dicto = super().omnigui_dict dicto = super().omnigui_instance_dict
dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr']) dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr'])
return dicto return dicto
@@ -1801,6 +1845,14 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
equipment_role = relationship(EquipmentRole, equipment_role = relationship(EquipmentRole,
back_populates="equipmentrole_submissiontype_associations") #: associated equipment back_populates="equipmentrole_submissiontype_associations") #: associated equipment
@hybrid_property
def submissiontype(self):
return self.submission_type
@hybrid_property
def equipmentrole(self):
return self.equipment_role
@validates('static') @validates('static')
def validate_static(self, key, value): def validate_static(self, key, value):
""" """
@@ -1847,6 +1899,8 @@ class Process(BaseClass):
A Process is a method used by a piece of equipment. A Process is a method used by a piece of equipment.
""" """
level = 2
id = Column(INTEGER, primary_key=True) #: Process id, primary key id = Column(INTEGER, primary_key=True) #: Process id, primary key
name = Column(String(64), unique=True) #: Process name name = Column(String(64), unique=True) #: Process name
submission_types = relationship("SubmissionType", back_populates='processes', submission_types = relationship("SubmissionType", back_populates='processes',
@@ -1869,14 +1923,24 @@ class Process(BaseClass):
""" """
return f"<Process({self.name})>" return f"<Process({self.name})>"
def set_attribute(self, key, value):
match key:
case "name":
self.name = value
case _:
field = getattr(self, key)
if value not in field:
field.append(value)
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
name: str | None = None, name: str | None = None,
id: int | None = None, id: int | None = None,
submission_type: str | SubmissionType | None = None, submissiontype: str | SubmissionType | None = None,
extraction_kit: str | KitType | None = None, kittype: str | KitType | None = None,
equipment_role: str | KitType | None = None, equipmentrole: str | KitType | None = None,
limit: int = 0, limit: int = 0,
**kwargs) -> Process | List[Process]: **kwargs) -> Process | List[Process]:
""" """
@@ -1891,28 +1955,28 @@ class Process(BaseClass):
Process|List[Process]: Process(es) matching criteria Process|List[Process]: Process(es) matching criteria
""" """
query = cls.__database_session__.query(cls) query = cls.__database_session__.query(cls)
match submission_type: match submissiontype:
case str(): case str():
submission_type = SubmissionType.query(name=submission_type) submissiontype = SubmissionType.query(name=submissiontype)
query = query.filter(cls.submission_types.contains(submission_type)) query = query.filter(cls.submission_types.contains(submissiontype))
case SubmissionType(): case SubmissionType():
query = query.filter(cls.submission_types.contains(submission_type)) query = query.filter(cls.submission_types.contains(submissiontype))
case _: case _:
pass pass
match extraction_kit: match kittype:
case str(): case str():
extraction_kit = KitType.query(name=extraction_kit) kittype = KitType.query(name=kittype)
query = query.filter(cls.kit_types.contains(extraction_kit)) query = query.filter(cls.kit_types.contains(kittype))
case KitType(): case KitType():
query = query.filter(cls.kit_types.contains(extraction_kit)) query = query.filter(cls.kit_types.contains(kittype))
case _: case _:
pass pass
match equipment_role: match equipmentrole:
case str(): case str():
equipment_role = EquipmentRole.query(name=equipment_role) equipmentrole = EquipmentRole.query(name=equipmentrole)
query = query.filter(cls.equipment_roles.contains(equipment_role)) query = query.filter(cls.equipment_roles.contains(equipmentrole))
case EquipmentRole(): case EquipmentRole():
query = query.filter(cls.equipment_roles.contains(equipment_role)) query = query.filter(cls.equipment_roles.contains(equipmentrole))
case _: case _:
pass pass
match name: match name:
@@ -1983,6 +2047,10 @@ class Tips(BaseClass, LogMixin):
submissions = association_proxy("tips_submission_associations", 'submission') submissions = association_proxy("tips_submission_associations", 'submission')
@hybrid_property
def tiprole(self):
return self.role
def __repr__(self): def __repr__(self):
return f"<Tips({self.name})>" return f"<Tips({self.name})>"
@@ -2033,6 +2101,14 @@ class SubmissionTypeTipRoleAssociation(BaseClass):
tip_role = relationship(TipRole, tip_role = relationship(TipRole,
back_populates="tiprole_submissiontype_associations") #: associated equipment back_populates="tiprole_submissiontype_associations") #: associated equipment
@hybrid_property
def submissiontype(self):
return self.submission_type
@hybrid_property
def tiprole(self):
return self.tip_role
@check_authorization @check_authorization
def save(self): def save(self):
super().save() super().save()

View File

@@ -6,6 +6,7 @@ import json, yaml, logging
from pathlib import Path from pathlib import Path
from pprint import pformat from pprint import pformat
from sqlalchemy import Column, String, INTEGER, ForeignKey, Table from sqlalchemy import Column, String, INTEGER, ForeignKey, Table
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, Query from sqlalchemy.orm import relationship, Query
from . import Base, BaseClass from . import Base, BaseClass
from tools import check_authorization, setup_lookup, yaml_regex_creator from tools import check_authorization, setup_lookup, yaml_regex_creator
@@ -38,6 +39,10 @@ class Organization(BaseClass):
contacts = relationship("Contact", back_populates="organization", contacts = relationship("Contact", back_populates="organization",
secondary=orgs_contacts) #: contacts involved with this org secondary=orgs_contacts) #: contacts involved with this org
@hybrid_property
def contact(self):
return self.contacts
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Organization({self.name})>" return f"<Organization({self.name})>"

View File

@@ -10,6 +10,9 @@ from zipfile import ZipFile, BadZipfile
from tempfile import TemporaryDirectory, TemporaryFile from tempfile import TemporaryDirectory, TemporaryFile
from operator import itemgetter from operator import itemgetter
from pprint import pformat from pprint import pformat
from sqlalchemy.ext.hybrid import hybrid_property
from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, LogMixin, SubmissionReagentAssociation from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, LogMixin, SubmissionReagentAssociation
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
@@ -126,6 +129,14 @@ class BasicSubmission(BaseClass, LogMixin):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Submission({self.rsl_plate_num})>" return f"<Submission({self.rsl_plate_num})>"
@hybrid_property
def kittype(self):
return self.extraction_kit
@hybrid_property
def organization(self):
return self.submitting_lab
@classproperty @classproperty
def jsons(cls) -> List[str]: def jsons(cls) -> List[str]:
""" """
@@ -488,7 +499,7 @@ class BasicSubmission(BaseClass, LogMixin):
""" """
# NOTE: use lookup function to create list of dicts # NOTE: use lookup function to create list of dicts
subs = [item.to_dict() for item in subs = [item.to_dict() for item in
cls.query(submission_type=submission_type, limit=limit, chronologic=chronologic, page=page, cls.query(submissiontype=submission_type, limit=limit, chronologic=chronologic, page=page,
page_size=page_size)] page_size=page_size)]
df = pd.DataFrame.from_records(subs) df = pd.DataFrame.from_records(subs)
# NOTE: Exclude sub information # NOTE: Exclude sub information
@@ -1056,7 +1067,7 @@ class BasicSubmission(BaseClass, LogMixin):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
submission_type: str | SubmissionType | None = None, submissiontype: str | SubmissionType | None = None,
submission_type_name: str | None = None, submission_type_name: str | None = None,
id: int | str | None = None, id: int | str | None = None,
rsl_plate_num: str | None = None, rsl_plate_num: str | None = None,
@@ -1087,8 +1098,8 @@ class BasicSubmission(BaseClass, LogMixin):
""" """
from ... import SubmissionReagentAssociation from ... import SubmissionReagentAssociation
# NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters # NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters
if submission_type is not None: if submissiontype is not None:
model = cls.find_polymorphic_subclass(polymorphic_identity=submission_type) model = cls.find_polymorphic_subclass(polymorphic_identity=submissiontype)
elif len(kwargs) > 0: elif len(kwargs) > 0:
# NOTE: find the subclass containing the relevant attributes # NOTE: find the subclass containing the relevant attributes
model = cls.find_polymorphic_subclass(attrs=kwargs) model = cls.find_polymorphic_subclass(attrs=kwargs)
@@ -1196,7 +1207,7 @@ class BasicSubmission(BaseClass, LogMixin):
if kwargs == {}: if kwargs == {}:
raise ValueError("Need to narrow down query or the first available instance will be returned.") raise ValueError("Need to narrow down query or the first available instance will be returned.")
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(submission_type=submission_type, limit=1, **sanitized_kwargs) instance = cls.query(submissiontype=submission_type, limit=1, **sanitized_kwargs)
if instance is None: if instance is None:
used_class = cls.find_polymorphic_subclass(attrs=kwargs, polymorphic_identity=submission_type) used_class = cls.find_polymorphic_subclass(attrs=kwargs, polymorphic_identity=submission_type)
instance = used_class(**sanitized_kwargs) instance = used_class(**sanitized_kwargs)

View File

@@ -157,7 +157,7 @@ class RSLNamer(object):
if "rsl_plate_num" in data.keys(): if "rsl_plate_num" in data.keys():
plate_number = data['rsl_plate_num'].split("-")[-1][0] plate_number = data['rsl_plate_num'].split("-")[-1][0]
else: else:
previous = BasicSubmission.query(start_date=today, end_date=today, submission_type=data['submission_type']) previous = BasicSubmission.query(start_date=today, end_date=today, submissiontype=data['submission_type'])
plate_number = len(previous) + 1 plate_number = len(previous) + 1
return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}" return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}"
@@ -191,5 +191,5 @@ class RSLNamer(object):
return "" return ""
from .pydant import PydSubmission, PydKit, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \
PydEquipment, PydEquipmentRole, PydTips, PydPCRControl, PydIridaControl PydEquipment, PydEquipmentRole, PydTips, PydPCRControl, PydIridaControl, PydProcess, PydElastic

View File

@@ -14,6 +14,8 @@ from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone
from backend.db.models import * from backend.db.models import *
from sqlalchemy.exc import StatementError, IntegrityError from sqlalchemy.exc import StatementError, IntegrityError
from sqlalchemy.orm.properties import ColumnProperty
from sqlalchemy.orm.relationships import _RelationshipDeclared
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -203,6 +205,7 @@ class PydSample(BaseModel, extra='allow'):
fields = list(self.model_fields.keys()) + list(self.model_extra.keys()) fields = list(self.model_fields.keys()) + list(self.model_extra.keys())
return {k: getattr(self, k) for k in fields} return {k: getattr(self, k) for k in fields}
@report_result
def to_sql(self, submission: BasicSubmission | str = None) -> Tuple[ def to_sql(self, submission: BasicSubmission | str = None) -> Tuple[
BasicSample, List[SubmissionSampleAssociation], Result | None]: BasicSample, List[SubmissionSampleAssociation], Result | None]:
""" """
@@ -270,6 +273,7 @@ class PydTips(BaseModel):
value = value.name value = value.name
return value return value
@report_result
def to_sql(self, submission: BasicSubmission) -> SubmissionTipsAssociation: def to_sql(self, submission: BasicSubmission) -> SubmissionTipsAssociation:
""" """
Convert this object to the SQL version for database storage. Convert this object to the SQL version for database storage.
@@ -280,12 +284,13 @@ class PydTips(BaseModel):
Returns: Returns:
SubmissionTipsAssociation: Association between queried tips and submission SubmissionTipsAssociation: Association between queried tips and submission
""" """
report = Report()
tips = Tips.query(name=self.name, limit=1) tips = Tips.query(name=self.name, limit=1)
# logger.debug(f"Tips query has yielded: {tips}") # logger.debug(f"Tips query has yielded: {tips}")
assoc = SubmissionTipsAssociation.query_or_create(tips=tips, submission=submission, role=self.role, limit=1) assoc = SubmissionTipsAssociation.query_or_create(tips=tips, submission=submission, role=self.role, limit=1)
# if assoc is None: # if assoc is None:
# assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role) # assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role)
return assoc return assoc, report
class PydEquipment(BaseModel, extra='ignore'): class PydEquipment(BaseModel, extra='ignore'):
@@ -317,6 +322,7 @@ class PydEquipment(BaseModel, extra='ignore'):
pass pass
return value return value
@report_result
def to_sql(self, submission: BasicSubmission | str = None, extraction_kit: KitType | str = None) -> Tuple[Equipment, SubmissionEquipmentAssociation]: def to_sql(self, submission: BasicSubmission | str = None, extraction_kit: KitType | str = None) -> Tuple[Equipment, SubmissionEquipmentAssociation]:
""" """
Creates Equipment and SubmssionEquipmentAssociations for this PydEquipment Creates Equipment and SubmssionEquipmentAssociations for this PydEquipment
@@ -327,6 +333,7 @@ class PydEquipment(BaseModel, extra='ignore'):
Returns: Returns:
Tuple[Equipment, SubmissionEquipmentAssociation]: SQL objects Tuple[Equipment, SubmissionEquipmentAssociation]: SQL objects
""" """
report = Report()
if isinstance(submission, str): if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission) submission = BasicSubmission.query(rsl_plate_num=submission)
if isinstance(extraction_kit, str): if isinstance(extraction_kit, str):
@@ -349,7 +356,7 @@ class PydEquipment(BaseModel, extra='ignore'):
# NOTE: It looks like the way fetching the processes is done in the SQL model, this shouldn't be a problem, but I'll include a failsafe. # NOTE: It looks like the way fetching the processes is done in the SQL model, this shouldn't be a problem, but I'll include a failsafe.
# NOTE: I need to find a way to filter this by the kit involved. # NOTE: I need to find a way to filter this by the kit involved.
if len(self.processes) > 1: if len(self.processes) > 1:
process = Process.query(submission_type=submission.get_submission_type(), extraction_kit=extraction_kit, equipment_role=self.role) process = Process.query(submissiontype=submission.get_submission_type(), kittype=extraction_kit, equipmentrole=self.role)
else: else:
process = Process.query(name=self.processes[0]) process = Process.query(name=self.processes[0])
if process is None: if process is None:
@@ -362,7 +369,7 @@ class PydEquipment(BaseModel, extra='ignore'):
else: else:
logger.warning(f"No submission found") logger.warning(f"No submission found")
assoc = None assoc = None
return equipment, assoc return equipment, assoc, report
def improved_dict(self) -> dict: def improved_dict(self) -> dict:
""" """
@@ -762,6 +769,7 @@ class PydSubmission(BaseModel, extra='allow'):
missing_reagents = [reagent for reagent in self.reagents if reagent.missing] missing_reagents = [reagent for reagent in self.reagents if reagent.missing]
return missing_info, missing_reagents return missing_info, missing_reagents
@report_result
def to_sql(self) -> Tuple[BasicSubmission, Report]: def to_sql(self) -> Tuple[BasicSubmission, Report]:
""" """
Converts this instance into a backend.db.models.submissions.BasicSubmission instance Converts this instance into a backend.db.models.submissions.BasicSubmission instance
@@ -867,7 +875,7 @@ class PydSubmission(BaseModel, extra='allow'):
# NOTE: Apply any discounts that are applicable for client and kit. # NOTE: Apply any discounts that are applicable for client and kit.
try: try:
discounts = [item.amount for item in discounts = [item.amount for item in
Discount.query(kit_type=instance.extraction_kit, organization=instance.submitting_lab)] Discount.query(kittype=instance.extraction_kit, organization=instance.submitting_lab)]
if len(discounts) > 0: if len(discounts) > 0:
instance.run_cost = instance.run_cost - sum(discounts) instance.run_cost = instance.run_cost - sum(discounts)
except Exception as e: except Exception as e:
@@ -1047,7 +1055,7 @@ class PydOrganization(BaseModel):
return None return None
return value return value
@report_result
def to_sql(self) -> Organization: def to_sql(self) -> Organization:
""" """
Converts this instance into a backend.db.models.organization.Organization instance. Converts this instance into a backend.db.models.organization.Organization instance.
@@ -1055,6 +1063,7 @@ class PydOrganization(BaseModel):
Returns: Returns:
Organization: Organization instance Organization: Organization instance
""" """
report = Report()
instance = Organization() instance = Organization()
for field in self.model_fields: for field in self.model_fields:
match field: match field:
@@ -1067,7 +1076,7 @@ class PydOrganization(BaseModel):
logger.debug(f"Setting {field} to {value}") logger.debug(f"Setting {field} to {value}")
if value: if value:
setattr(instance, field, value) setattr(instance, field, value)
return instance return instance, report
class PydReagentRole(BaseModel): class PydReagentRole(BaseModel):
@@ -1083,6 +1092,7 @@ class PydReagentRole(BaseModel):
return timedelta(days=value) return timedelta(days=value)
return value return value
@report_result
def to_sql(self, kit: KitType) -> ReagentRole: def to_sql(self, kit: KitType) -> ReagentRole:
""" """
Converts this instance into a backend.db.models.ReagentType instance Converts this instance into a backend.db.models.ReagentType instance
@@ -1093,23 +1103,25 @@ class PydReagentRole(BaseModel):
Returns: Returns:
ReagentRole: ReagentType instance ReagentRole: ReagentType instance
""" """
report = Report()
instance: ReagentRole = ReagentRole.query(name=self.name) instance: ReagentRole = ReagentRole.query(name=self.name)
if instance is None: if instance is None:
instance = ReagentRole(name=self.name, eol_ext=self.eol_ext) instance = ReagentRole(name=self.name, eol_ext=self.eol_ext)
try: try:
assoc = KitTypeReagentRoleAssociation.query(reagent_role=instance, kit_type=kit) assoc = KitTypeReagentRoleAssociation.query(reagentrole=instance, kittype=kit)
except StatementError: except StatementError:
assoc = None assoc = None
if assoc is None: if assoc is None:
assoc = KitTypeReagentRoleAssociation(kit_type=kit, reagent_role=instance, uses=self.uses, assoc = KitTypeReagentRoleAssociation(kit_type=kit, reagent_role=instance, uses=self.uses,
required=self.required) required=self.required)
return instance return instance, report
class PydKit(BaseModel): class PydKitType(BaseModel):
name: str name: str
reagent_roles: List[PydReagentRole] = [] reagent_roles: List[PydReagentRole] = []
@report_result
def to_sql(self) -> Tuple[KitType, Report]: def to_sql(self) -> Tuple[KitType, Report]:
""" """
Converts this instance into a backend.db.models.kits.KitType instance Converts this instance into a backend.db.models.kits.KitType instance
@@ -1163,7 +1175,9 @@ class PydPCRControl(BaseModel):
submission_id: int submission_id: int
controltype_name: str controltype_name: str
@report_result
def to_sql(self): def to_sql(self):
report = Report
instance = PCRControl.query(name=self.name) instance = PCRControl.query(name=self.name)
if not instance: if not instance:
instance = PCRControl() instance = PCRControl()
@@ -1171,7 +1185,7 @@ class PydPCRControl(BaseModel):
field_value = self.__getattribute__(key) field_value = self.__getattribute__(key)
if instance.__getattribute__(key) != field_value: if instance.__getattribute__(key) != field_value:
instance.__setattr__(key, field_value) instance.__setattr__(key, field_value)
return instance return instance, report
class PydIridaControl(BaseModel, extra='ignore'): class PydIridaControl(BaseModel, extra='ignore'):
@@ -1196,7 +1210,9 @@ class PydIridaControl(BaseModel, extra='ignore'):
value = "" value = ""
return value return value
@report_result
def to_sql(self): def to_sql(self):
report = Report()
instance = IridaControl.query(name=self.name) instance = IridaControl.query(name=self.name)
if not instance: if not instance:
instance = IridaControl() instance = IridaControl()
@@ -1204,7 +1220,7 @@ class PydIridaControl(BaseModel, extra='ignore'):
field_value = self.__getattribute__(key) field_value = self.__getattribute__(key)
if instance.__getattribute__(key) != field_value: if instance.__getattribute__(key) != field_value:
instance.__setattr__(key, field_value) instance.__setattr__(key, field_value)
return instance return instance, report
class PydProcess(BaseModel, extra="allow"): class PydProcess(BaseModel, extra="allow"):
@@ -1222,16 +1238,62 @@ class PydProcess(BaseModel, extra="allow"):
return [value] return [value]
return value return value
@report_result
def to_sql(self): def to_sql(self):
report = Report()
instance = Process.query(name=self.name) instance = Process.query(name=self.name)
if not instance: if not instance:
instance = Process() instance = Process()
dicto = instance.omnigui_dict # dicto = instance.omnigui_instance_dict
for key in self.model_fields: fields = [item for item in self.model_fields]
# field_value = self.__getattribute__(key) for field in fields:
# if instance.__getattribute__(key) != field_value: logger.debug(f"Field: {field}")
# instance.__setattr__(key, field_value) try:
test = dicto[key] field_type = getattr(instance.__class__, field).property
print(f"Attribute: {test['class_attr'].property}") except AttributeError:
logger.error(f"No attribute: {field} in {instance.__class__}")
continue
match field_type:
case _RelationshipDeclared():
logger.debug(f"{field} is a relationship with {field_type.entity.class_}")
query_str = getattr(self, field)
if isinstance(query_str, list):
query_str = query_str[0]
if query_str in ["", " ", None]:
continue
logger.debug(f"Querying {field_type.entity.class_} with name {query_str}")
field_value = field_type.entity.class_.query(name=query_str)
logger.debug(f"{field} query result: {field_value}")
case ColumnProperty():
logger.debug(f"{field} is a property.")
field_value = getattr(self, field)
instance.set_attribute(key=field, value=field_value)
return instance, report
class PydElastic(BaseModel, extra="allow", arbitrary_types_allowed=True):
"""Allows for creation of arbitrary pydantic models"""
instance: BaseClass
@report_result
def to_sql(self):
print(self.instance)
fields = [item for item in self.model_extra]
for field in fields:
try:
field_type = getattr(self.instance.__class__, field).property
except AttributeError:
logger.error(f"No attribute: {field} in {self.instance.__class__}")
continue
match field_type:
case _RelationshipDeclared():
logger.debug(f"{field} is a relationship with {field_type.entity.class_}")
field_value = field_type.entity.class_.argument.query(name=getattr(self, field))
logger.debug(f"{field} query result: {field_value}")
case ColumnProperty():
logger.debug(f"{field} is a property.")
field_value = getattr(self, field)
self.instance.__setattr__(field, field_value)
return self.instance
return instance

View File

@@ -1,6 +1,7 @@
""" """
A widget to handle adding/updating any database object. A widget to handle adding/updating any database object.
""" """
from copy import deepcopy
from datetime import date from datetime import date
from pprint import pformat from pprint import pformat
from typing import Any, Tuple from typing import Any, Tuple
@@ -14,7 +15,6 @@ from sqlalchemy.orm import InstrumentedAttribute, ColumnProperty
import logging import logging
from sqlalchemy.orm.relationships import _RelationshipDeclared from sqlalchemy.orm.relationships import _RelationshipDeclared
from tools import Report, report_result from tools import Report, report_result
from backend import db
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -26,9 +26,11 @@ class AddEdit(QDialog):
logger.debug(f"Managers: {managers}") logger.debug(f"Managers: {managers}")
self.instance = instance self.instance = instance
self.object_type = instance.__class__ self.object_type = instance.__class__
# self.managers = deepcopy(managers)
self.managers = managers self.managers = managers
if instance.level < 2: if instance.level < 2:
try: try:
logger.debug(f"Parent instance: {self.parent().instance}")
self.managers.add(self.parent().instance) self.managers.add(self.parent().instance)
except AttributeError: except AttributeError:
pass pass
@@ -38,8 +40,8 @@ class AddEdit(QDialog):
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
logger.debug(f"Fields: {pformat(self.instance.omnigui_dict)}") # logger.debug(f"Fields: {pformat(self.instance.omnigui_instance_dict)}")
fields = {k: v for k, v in self.instance.omnigui_dict.items() if "id" not in k} fields = {k: v for k, v in self.instance.omnigui_instance_dict.items() if "id" not in k}
# NOTE: Move 'name' to the front # NOTE: Move 'name' to the front
try: try:
fields = {'name': fields.pop('name'), **fields} fields = {'name': fields.pop('name'), **fields}
@@ -67,12 +69,15 @@ class AddEdit(QDialog):
@report_result @report_result
def parse_form(self) -> Tuple[BaseModel, Report]: def parse_form(self) -> Tuple[BaseModel, Report]:
from backend.validators import pydant
report = Report() report = Report()
parsed = {result[0].strip(":"): result[1] for result in parsed = {result[0].strip(":"): result[1] for result in
[item.parse_form() for item in self.findChildren(EditProperty)] if result[0]} [item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}
logger.debug(f"Parsed form: {parsed}") logger.debug(f"Parsed form: {parsed}")
model = self.object_type.pydantic_model model = self.object_type.pydantic_model
logger.debug(f"Model type: {model.__name__}")
if model.__name__ == "PydElastic":
logger.debug(f"We have an elastic model.")
parsed['instance'] = self.instance
# NOTE: Hand-off to pydantic model for validation. # NOTE: Hand-off to pydantic model for validation.
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts. # NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts.
model = model(**parsed) model = model(**parsed)

View File

@@ -1,6 +1,8 @@
""" """
Provides a screen for managing all attributes of a database object. Provides a screen for managing all attributes of a database object.
""" """
from copy import deepcopy
from pprint import pformat
from typing import Any, List from typing import Any, List
from PyQt6.QtCore import QSortFilterProxyModel, Qt from PyQt6.QtCore import QSortFilterProxyModel, Qt
from PyQt6.QtGui import QAction, QCursor from PyQt6.QtGui import QAction, QCursor
@@ -17,6 +19,8 @@ from sqlalchemy.orm.relationships import _RelationshipDeclared
from pandas import DataFrame from pandas import DataFrame
from backend import db from backend import db
import logging import logging
from tools import check_object_in_managers
from .omni_add_edit import AddEdit from .omni_add_edit import AddEdit
from .omni_search import SearchBox from .omni_search import SearchBox
from frontend.widgets.submission_table import pandasModel from frontend.widgets.submission_table import pandasModel
@@ -33,6 +37,7 @@ class ManagerWindow(QDialog):
super().__init__(parent) super().__init__(parent)
self.object_type = self.original_type = object_type self.object_type = self.original_type = object_type
self.instance = None self.instance = None
# self.managers = deepcopy(managers)
self.managers = managers self.managers = managers
try: try:
self.managers.add(self.parent().instance) self.managers.add(self.parent().instance)
@@ -68,28 +73,35 @@ class ManagerWindow(QDialog):
""" """
Changes form inputs based on sample type Changes form inputs based on sample type
""" """
# logger.debug(f"Instance: {self.instance}")
if self.sub_class: if self.sub_class:
self.object_type = getattr(db, self.sub_class.currentText()) self.object_type = getattr(db, self.sub_class.currentText())
logger.debug(f"From update options, managers: {self.managers}") # logger.debug(f"From update options, managers: {self.managers}")
try: try:
query_kwargs = {self.parent().instance.query_alias: self.parent().instance} query_kwargs = {self.parent().instance.query_alias: self.parent().instance}
except AttributeError as e: except AttributeError as e:
logger.debug(f"Couldn't set query kwargs due to: {e}") # logger.debug(f"Couldn't set query kwargs due to: {e}")
query_kwargs = {} query_kwargs = {}
logger.debug(f"Query kwargs: {query_kwargs}") # logger.debug(f"Query kwargs: {query_kwargs}")
options = [item.name for item in self.object_type.query(**query_kwargs)] options = [item.name for item in self.object_type.query(**query_kwargs)]
logger.debug(f"self.object_type: {self.object_type}") # logger.debug(f"self.object_type: {self.object_type}")
if self.instance: if self.instance:
options.insert(0, options.pop(options.index(self.instance.name))) try:
inserter = options.pop(options.index(self.instance.name))
except ValueError:
inserter = self.instance.name
options.insert(0, inserter)
self.options.clear() self.options.clear()
self.options.addItems(options) self.options.addItems(options)
self.options.setEditable(False) self.options.setEditable(False)
self.options.setMinimumWidth(self.minimumWidth()) self.options.setMinimumWidth(self.minimumWidth())
self.layout.addWidget(self.options, 1, 0, 1, 1) self.layout.addWidget(self.options, 1, 0, 1, 1)
if len(options) > 0:
self.add_button = QPushButton("Add New") self.add_button = QPushButton("Add New")
self.layout.addWidget(self.add_button, 1, 1, 1, 1) self.layout.addWidget(self.add_button, 1, 1, 1, 1)
self.options.currentTextChanged.connect(self.update_data)
self.add_button.clicked.connect(self.add_new) self.add_button.clicked.connect(self.add_new)
self.options.currentTextChanged.connect(self.update_data)
# logger.debug(f"Instance: {self.instance}")
self.update_data() self.update_data()
def update_data(self) -> None: def update_data(self) -> None:
@@ -105,26 +117,34 @@ class ManagerWindow(QDialog):
[item for item in self.findChildren(QDialogButtonBox)] [item for item in self.findChildren(QDialogButtonBox)]
for item in deletes: for item in deletes:
item.setParent(None) item.setParent(None)
# logger.debug(f"Current options text lower: {self.options.currentText().lower()}")
# NOTE: Find the instance this manager will update # NOTE: Find the instance this manager will update
logger.debug(f"Querying with {self.options.currentText()}") if "blank" not in self.options.currentText().lower() and self.options.currentText() != "":
# logger.debug(f"Querying with {self.options.currentText()}")
self.instance = self.object_type.query(name=self.options.currentText(), limit=1) self.instance = self.object_type.query(name=self.options.currentText(), limit=1)
logger.debug(f"Instance: {self.instance}") # logger.debug(f"Instance: {self.instance}")
fields = {k: v for k, v in self.instance.omnigui_dict.items() if if not self.instance:
self.instance = self.object_type()
# logger.debug(f"self.instance: {self.instance}")
fields = {k: v for k, v in self.instance.omnigui_instance_dict.items() if
isinstance(v['class_attr'], InstrumentedAttribute) and k != "id"} isinstance(v['class_attr'], InstrumentedAttribute) and k != "id"}
# logger.debug(f"Instance fields: {fields}")
for key, field in fields.items(): for key, field in fields.items():
try:
value = getattr(self.instance, key)
except AttributeError:
value = None
match field['class_attr'].property: match field['class_attr'].property:
# NOTE: ColumnProperties will be directly edited. # NOTE: ColumnProperties will be directly edited.
case ColumnProperty(): case ColumnProperty():
# NOTE: field.property.expression.type gives db column type eg. STRING or TIMESTAMP # NOTE: field.property.expression.type gives db column type eg. STRING or TIMESTAMP
widget = EditProperty(self, key=key, column_type=field, widget = EditProperty(self, key=key, column_type=field,
value=getattr(self.instance, key)) value=value)
# NOTE: RelationshipDeclareds will be given a list of existing related objects. # NOTE: RelationshipDeclareds will be given a list of existing related objects.
case _RelationshipDeclared(): case _RelationshipDeclared():
if key != "submissions": if key != "submissions":
# NOTE: field.comparator.entity.class_ gives the relationship class # NOTE: field.comparator.entity.class_ gives the relationship class
widget = EditRelationship(self, key=key, entity=field['class_attr'].comparator.entity.class_, widget = EditRelationship(self, key=key, entity=field['class_attr'].comparator.entity.class_,
value=getattr(self.instance, key)) value=value)
else: else:
continue continue
case _: case _:
@@ -144,16 +164,26 @@ class ManagerWindow(QDialog):
# TODO: Need Relationship property here too? # TODO: Need Relationship property here too?
results = [item.parse_form() for item in self.findChildren(EditProperty)] results = [item.parse_form() for item in self.findChildren(EditProperty)]
for result in results: for result in results:
# logger.debug(result) logger.debug(f"Incoming result: {result}")
self.instance.__setattr__(result[0], result[1]) setattr(self.instance, result['field'], result['value'])
logger.debug(f"Set result: {getattr(self.instance, result['field'])}")
results = [item.parse_form() for item in self.findChildren(EditRelationship)]
for result in results:
logger.debug(f"Incoming result: {result}")
setattr(self.instance, result['field'], result['value'])
logger.debug(f"Set result: {getattr(self.instance, result['field'])}")
return self.instance return self.instance
def add_new(self): def add_new(self):
dlg = AddEdit(parent=self, instance=self.object_type(), managers=self.managers) # dlg = AddEdit(parent=self, instance=self.object_type(), managers=self.managers)
if dlg.exec(): # if dlg.exec():
new_pyd = dlg.parse_form() # new_pyd = dlg.parse_form()
new_instance = new_pyd.to_sql() # new_instance = new_pyd.to_sql()
new_instance.save() # # new_instance.save()
# logger.debug(f"New instance: {new_instance}")
# self.instance = new_instance
# self.update_options()
new_instance = self.object_type()
self.instance = new_instance self.instance = new_instance
self.update_options() self.update_options()
@@ -165,7 +195,8 @@ class EditProperty(QWidget):
self.label = QLabel(key.title().replace("_", " ")) self.label = QLabel(key.title().replace("_", " "))
self.layout = QGridLayout() self.layout = QGridLayout()
self.layout.addWidget(self.label, 0, 0, 1, 1) self.layout.addWidget(self.label, 0, 0, 1, 1)
logger.debug(f"Column type: {column_type}") self.setObjectName(key)
# logger.debug(f"Column type: {column_type}")
match column_type['class_attr'].property.expression.type: match column_type['class_attr'].property.expression.type:
case String(): case String():
self.widget = QLineEdit(self) self.widget = QLineEdit(self)
@@ -203,9 +234,13 @@ class EditProperty(QWidget):
value = self.widget.text() value = self.widget.text()
case QDateEdit(): case QDateEdit():
value = self.widget.date() value = self.widget.date()
case QSpinBox() | QDoubleSpinBox():
value = self.widget.value()
case QCheckBox():
value = self.widget.isChecked()
case _: case _:
value = None value = None
return self.objectName(), value return dict(field=self.objectName(), value=value)
class EditRelationship(QWidget): class EditRelationship(QWidget):
@@ -213,25 +248,37 @@ class EditRelationship(QWidget):
def __init__(self, parent, key: str, entity: Any, value): def __init__(self, parent, key: str, entity: Any, value):
super().__init__(parent) super().__init__(parent)
self.entity = entity #: The class of interest self.entity = entity #: The class of interest
self.data = value self.setParent(parent)
# logger.debug(f"Edit relationship entity: {self.entity}")
self.label = QLabel(key.title().replace("_", " ")) self.label = QLabel(key.title().replace("_", " "))
self.setObjectName(key) self.setObjectName(key) #: key is the name of the relationship this represents
self.table = QTableView() self.relationship = getattr(self.parent().instance.__class__, key)
# logger.debug(f"self.relationship: {self.relationship}")
# logger.debug(f"Relationship uses list: {self.relationship.property.uselist}")
self.data = value
# logger.debug(f"Data for edit relationship: {self.data}")
self.widget = QTableView()
self.set_data()
self.add_button = QPushButton("Add New") self.add_button = QPushButton("Add New")
self.add_button.clicked.connect(self.add_new) self.add_button.clicked.connect(self.add_new)
self.existing_button = QPushButton("Add Existing") self.existing_button = QPushButton("Add Existing")
self.existing_button.clicked.connect(self.add_existing) self.existing_button.clicked.connect(self.add_existing)
self.existing_button.setEnabled(self.entity.level == 1) # self.existing_button.setEnabled(self.entity.level == 1)
if not self.relationship.property.uselist and len(self.data) >= 1:
self.add_button.setEnabled(False)
self.existing_button.setEnabled(False)
logger.debug(f"Checked manager for check: {check_object_in_managers(self.parent().managers, self.objectName())}")
if check_object_in_managers(self.parent().managers, self.objectName()):
self.widget.setEnabled(False)
self.add_button.setEnabled(False)
self.existing_button.setEnabled(False)
self.layout = QGridLayout() self.layout = QGridLayout()
self.layout.addWidget(self.label, 0, 0, 1, 5) self.layout.addWidget(self.label, 0, 0, 1, 5)
self.layout.addWidget(self.table, 1, 0, 1, 8) self.layout.addWidget(self.widget, 1, 0, 1, 8)
self.layout.addWidget(self.add_button, 0, 6, 1, 1, alignment=Qt.AlignmentFlag.AlignRight) self.layout.addWidget(self.add_button, 0, 6, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
self.layout.addWidget(self.existing_button, 0, 7, 1, 1, alignment=Qt.AlignmentFlag.AlignRight) self.layout.addWidget(self.existing_button, 0, 7, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
self.setLayout(self.layout) self.setLayout(self.layout)
self.set_data() # self.set_data()
self.table.resizeColumnsToContents()
self.table.resizeRowsToContents()
self.table.setSortingEnabled(True)
def parse_row(self, x): def parse_row(self, x):
context = {item: x.sibling(x.row(), self.data.columns.get_loc(item)).data() for item in self.data.columns} context = {item: x.sibling(x.row(), self.data.columns.get_loc(item)).data() for item in self.data.columns}
@@ -239,34 +286,42 @@ class EditRelationship(QWidget):
object = self.entity.query(**context) object = self.entity.query(**context)
except KeyError: except KeyError:
object = None object = None
self.table.doubleClicked.disconnect() self.widget.doubleClicked.disconnect()
self.add_edit(instance=object) self.add_edit(instance=object)
def add_new(self, instance: Any = None): def add_new(self, instance: Any = None):
# NOTE: if an existing instance is not being edited, create a new instance # NOTE: if an existing instance is not being edited, create a new instance
if not instance: if not instance:
instance = self.entity() instance = self.entity()
# if self.parent().object_type.level == 2:
managers = self.parent().managers managers = self.parent().managers
# else: logger.debug(f"Managers going into add new: {managers}")
# managers = self.parent().managers + [self.parent().instance] # match instance.level:
match instance.level: # case 1:
case 1: # dlg = AddEdit(self.parent(), instance=instance, managers=managers)
dlg = AddEdit(self.parent(), instance=instance, managers=managers) # case 2:
case 2: # dlg = ManagerWindow(self.parent(), object_type=instance.__class__, extras=[], managers=managers)
# case _:
# return
dlg = ManagerWindow(self.parent(), object_type=instance.__class__, extras=[], managers=managers) dlg = ManagerWindow(self.parent(), object_type=instance.__class__, extras=[], managers=managers)
case _:
return
if dlg.exec(): if dlg.exec():
new_instance = dlg.parse_form() new_instance = dlg.parse_form()
logger.debug(f"New instance before transformation attempt: {new_instance}")
try:
new_instance = new_instance.to_sql() new_instance = new_instance.to_sql()
logger.debug(f"New instance: {new_instance}") except AttributeError as e:
addition = getattr(self.parent().instance, self.objectName()) logger.error(f"Couldn't convert {new_instance} to sql due to {e}")
logger.debug(f"Addition: {addition}") logger.debug(f"New instance after transformation attempt: {new_instance.__dict__}")
# addition = getattr(self.parent().instance, self.objectName())
# logger.debug(f"Addition: {addition}")
# if self.relationship.property.uselist:
# addition.append(instance)
# else:
# addition = instance
# setattr(self.parent().instance, self.objectName(), new_instance)
logger.debug(f"Parent instance after insert: {getattr(self.parent().instance, self.objectName())}")
# NOTE: Saving currently disabled # NOTE: Saving currently disabled
# if isinstance(addition, InstrumentedList):
# addition.append(new_instance)
# self.parent().instance.save() # self.parent().instance.save()
return new_instance
self.parent().update_data() self.parent().update_data()
def add_existing(self): def add_existing(self):
@@ -274,16 +329,24 @@ class EditRelationship(QWidget):
if dlg.exec(): if dlg.exec():
rows = dlg.return_selected_rows() rows = dlg.return_selected_rows()
for row in rows: for row in rows:
logger.debug(f"Querying with {row}") # logger.debug(f"Querying with {row}")
instance = self.entity.query(**row) instance = self.entity.query(**row)
logger.debug(f"Queried instance: {instance}") # logger.debug(f"Queried instance: {instance}")
addition = getattr(self.parent().instance, self.objectName()) # logger.debug(f"Checking field type: {self.objectName()}")
logger.debug(f"Addition: {addition}") # addition = getattr(self.parent().instance, self.objectName())
# logger.debug(f"Instance object: {addition}")
# NOTE: Saving currently disabled # NOTE: Saving currently disabled
# if isinstance(addition, InstrumentedList): # if self.relationship.property.uselist:
# addition.append(instance) # addition.append(instance)
# else:
# addition = instance
setattr(self.parent().instance, self.objectName(), instance)
# self.parent().instance.save() # self.parent().instance.save()
self.parent().update_data() # self.parent().update_data()
# yield instance
def set_choices(self) -> None:
pass
def set_data(self) -> None: def set_data(self) -> None:
""" """
@@ -291,26 +354,38 @@ class EditRelationship(QWidget):
""" """
# logger.debug(self.data) # logger.debug(self.data)
if not isinstance(self.data, list): if not isinstance(self.data, list):
if self.data is not None:
self.data = [self.data] self.data = [self.data]
records = [{k: v['instance_attr'] for k, v in item.omnigui_dict.items()} for item in self.data] else:
# logger.debug(f"Records: {records}") self.data = []
self.data = DataFrame.from_records(records) checked_manager = check_object_in_managers(self.parent().managers, self.objectName())
logger.debug(f"Returned checked_manager: {checked_manager}")
if checked_manager is not None:
if not self.data:
self.data = [checked_manager]
# logger.debug(f"Data: {self.data}")
try: try:
self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras] records = [{k: v['instance_attr'] for k, v in item.omnigui_instance_dict.items()} for item in self.data]
except AttributeError:
records = []
# logger.debug(f"Records: {records}")
self.df = DataFrame.from_records(records)
try:
self.columns_of_interest = [dict(name=item, column=self.df.columns.get_loc(item)) for item in self.extras]
except (KeyError, AttributeError): except (KeyError, AttributeError):
self.columns_of_interest = [] self.columns_of_interest = []
try: try:
self.data['id'] = self.data['id'].apply(str) self.df['id'] = self.df['id'].apply(str)
self.data['id'] = self.data['id'].str.zfill(4) self.df['id'] = self.df['id'].str.zfill(4)
except KeyError as e: except KeyError as e:
logger.error(f"Could not alter id to string due to KeyError: {e}") logger.error(f"Could not alter id to string due to KeyError: {e}")
proxy_model = QSortFilterProxyModel() proxy_model = QSortFilterProxyModel()
proxy_model.setSourceModel(pandasModel(self.data)) proxy_model.setSourceModel(pandasModel(self.df))
self.table.setModel(proxy_model) self.widget.setModel(proxy_model)
self.table.resizeColumnsToContents() self.widget.resizeColumnsToContents()
self.table.resizeRowsToContents() self.widget.resizeRowsToContents()
self.table.setSortingEnabled(True) self.widget.setSortingEnabled(True)
self.table.doubleClicked.connect(self.parse_row) self.widget.doubleClicked.connect(self.parse_row)
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
""" """
@@ -319,16 +394,23 @@ class EditRelationship(QWidget):
Args: Args:
event (_type_): the item of interest event (_type_): the item of interest
""" """
id = self.table.selectionModel().currentIndex() print(self.widget.isEnabled())
if not self.widget.isEnabled():
logger.warning(f"{self.objectName()} is disabled.")
return
id = self.widget.selectionModel().currentIndex()
# NOTE: the overly complicated {column_name: row_value} dictionary construction # NOTE: the overly complicated {column_name: row_value} dictionary construction
row_data = {self.data.columns[column]: self.table.model().index(id.row(), column).data() for column in row_data = {self.df.columns[column]: self.widget.model().index(id.row(), column).data() for column in
range(self.table.model().columnCount())} range(self.widget.model().columnCount())}
object = self.entity.query(**row_data) object = self.entity.query(**row_data)
if isinstance(object, list): if isinstance(object, list):
object = object[0] object = object[0]
logger.debug(object) # logger.debug(object)
self.menu = QMenu(self) self.menu = QMenu(self)
try:
action = QAction(f"Remove {object.name}", self) action = QAction(f"Remove {object.name}", self)
except AttributeError:
action = QAction(f"Remove object", self)
action.triggered.connect(lambda: self.remove_item(object=object)) action.triggered.connect(lambda: self.remove_item(object=object))
self.menu.addAction(action) self.menu.addAction(action)
self.menu.popup(QCursor.pos()) self.menu.popup(QCursor.pos())
@@ -338,3 +420,6 @@ class EditRelationship(QWidget):
editor.remove(object) editor.remove(object)
self.parent().instance.save() self.parent().instance.save()
self.parent().update_data() self.parent().update_data()
def parse_form(self):
return dict(field=self.objectName(), value=self.data)

View File

@@ -341,7 +341,7 @@ class SubmissionFormWidget(QWidget):
# result = self.pyd.check_reagent_expiries(exempt=exempt) # result = self.pyd.check_reagent_expiries(exempt=exempt)
if len(result.results) > 0: if len(result.results) > 0:
return report return report
base_submission, result = self.pyd.to_sql() base_submission = self.pyd.to_sql()
# NOTE: check output message for issues # NOTE: check output message for issues
try: try:
trigger = result.results[-1] trigger = result.results[-1]
@@ -693,7 +693,7 @@ class SubmissionFormWidget(QWidget):
# NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly. # NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
rt = ReagentRole.query(name=self.reagent.role) rt = ReagentRole.query(name=self.reagent.role)
if rt is None: if rt is None:
rt = ReagentRole.query(kit_type=self.extraction_kit, reagent=wanted_reagent) rt = ReagentRole.query(kittype=self.extraction_kit, reagent=wanted_reagent)
final = PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, role=rt.name, final = PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, role=rt.name,
expiry=wanted_reagent.expiry.date(), missing=False) expiry=wanted_reagent.expiry.date(), missing=False)
return final, report return final, report
@@ -733,8 +733,8 @@ class SubmissionFormWidget(QWidget):
def __init__(self, scrollWidget, reagent, extraction_kit: str) -> None: def __init__(self, scrollWidget, reagent, extraction_kit: str) -> None:
super().__init__(scrollWidget=scrollWidget) super().__init__(scrollWidget=scrollWidget)
self.setEditable(True) self.setEditable(True)
looked_up_rt = KitTypeReagentRoleAssociation.query(reagent_role=reagent.role, looked_up_rt = KitTypeReagentRoleAssociation.query(reagentrole=reagent.role,
kit_type=extraction_kit) kittype=extraction_kit)
relevant_reagents = [str(item.lot) for item in looked_up_rt.get_all_relevant_reagents()] relevant_reagents = [str(item.lot) for item in looked_up_rt.get_all_relevant_reagents()]
# NOTE: if reagent in sheet is not found insert it into the front of relevant reagents so it shows # NOTE: if reagent in sheet is not found insert it into the front of relevant reagents so it shows
if str(reagent.lot) not in relevant_reagents: if str(reagent.lot) not in relevant_reagents:

View File

@@ -16,11 +16,14 @@ from dateutil.easter import easter
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from logging import handlers from logging import handlers
from pathlib import Path from pathlib import Path
from sqlalchemy.orm import Session from sqlalchemy.orm import Session, InstrumentedAttribute
from sqlalchemy import create_engine, text, MetaData from sqlalchemy import create_engine, text, MetaData
from pydantic import field_validator, BaseModel, Field from pydantic import field_validator, BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Any, Tuple, Literal, List, Generator from typing import Any, Tuple, Literal, List, Generator
from sqlalchemy.orm.relationships import _RelationshipDeclared
from __init__ import project_path from __init__ import project_path
from configparser import ConfigParser from configparser import ConfigParser
from tkinter import Tk # NOTE: This is for choosing database path before app is created. from tkinter import Tk # NOTE: This is for choosing database path before app is created.
@@ -813,6 +816,37 @@ def setup_lookup(func):
return wrapper return wrapper
def check_object_in_managers(managers: list, object_name: object):
for manager in managers:
logger.debug(f"Manager: {manager}, Key: {object_name}")
if object_name in manager.aliases:
return manager
relationships = [getattr(manager.__class__, item) for item in dir(manager.__class__)
if isinstance(getattr(manager.__class__, item), InstrumentedAttribute)]
relationships = [item for item in relationships if isinstance(item.property, _RelationshipDeclared)]
for relationship in relationships:
if relationship.key == object_name:
logger.debug(f"Checking {relationship.key}")
try:
rel_obj = getattr(manager, relationship.key)
if rel_obj is not None:
logger.debug(f"Returning {rel_obj}")
return rel_obj
except AttributeError:
pass
if "association" in relationship.key:
try:
logger.debug(f"Checking association {relationship.key}")
rel_obj = next((getattr(item, object_name) for item in getattr(manager, relationship.key)
if getattr(item, object_name) is not None), None)
if rel_obj is not None:
logger.debug(f"Returning {rel_obj}")
return rel_obj
except AttributeError:
pass
return None
def get_application_from_parent(widget): def get_application_from_parent(widget):
try: try:
return widget.app return widget.app