From 919376c1ce29a9cf63d04e3f93a90c7de0faaec5 Mon Sep 17 00:00:00 2001 From: lwark Date: Fri, 7 Mar 2025 10:19:12 -0600 Subject: [PATCH] Pydantic switchover debugging. --- src/scripts/import_irida.py | 7 +- src/submissions/backend/db/models/__init__.py | 15 +- src/submissions/backend/db/models/kits.py | 47 +- .../backend/db/models/submissions.py | 2 + .../backend/validators/omni_gui_objects.py | 579 +++++++++++++++ src/submissions/frontend/widgets/app.py | 6 + .../frontend/widgets/omni_manager_pydant.py | 680 ++++++++++++++++++ 7 files changed, 1327 insertions(+), 9 deletions(-) create mode 100644 src/submissions/backend/validators/omni_gui_objects.py create mode 100644 src/submissions/frontend/widgets/omni_manager_pydant.py diff --git a/src/scripts/import_irida.py b/src/scripts/import_irida.py index 3ea84dd..41eb48e 100644 --- a/src/scripts/import_irida.py +++ b/src/scripts/import_irida.py @@ -52,7 +52,12 @@ def import_irida(ctx: Settings): sample = new_session.query(BasicSample).filter(BasicSample.submitter_id == instance.name).first() if sample: instance.sample = sample - instance.submission = sample.submissions[0] + try: + instance.submission = sample.submissions[0] + except IndexError: + logger.error(f"Could not get sample for {sample}") + instance.submission = None + # instance.submission = sample.submission[0] new_session.add(instance) new_session.commit() new_session.close() diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index 4dba0f6..db91ab0 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -397,8 +397,8 @@ class BaseClass(Base): """ Custom dunder method to handle potential list relationship issues. """ - if key != "_sa_instance_state": - logger.debug(f"Attempting to set {key} to {pformat(value)}") + # if key != "_sa_instance_state": + # logger.debug(f"Attempting to set {key} to {pformat(value)}") try: field_type = getattr(self.__class__, key) except AttributeError: @@ -407,21 +407,25 @@ class BaseClass(Base): logger.debug(f"{key} is an InstrumentedAttribute.") match field_type.property: case ColumnProperty(): - logger.debug(f"Setting ColumnProperty to {value}") + # logger.debug(f"Setting ColumnProperty to {value}") return super().__setattr__(key, value) case _RelationshipDeclared(): - logger.debug(f"Setting _RelationshipDeclared to {value}") + logger.debug(f"{self.__class__.__name__} Setting _RelationshipDeclared for {key} to {value}") if field_type.property.uselist: logger.debug(f"Setting with uselist") existing = self.__getattribute__(key) + # NOTE: This is causing problems with removal of items from lists. Have to overhaul it. if existing is not None: + logger.debug(f"{key} Existing: {existing}, incoming: {value}") if isinstance(value, list): - value = existing + value + # value = existing + value + value = value else: value = existing + [value] else: value = [value] value = list(set(value)) + logger.debug(f"Final value for {key}: {value}") return super().__setattr__(key, value) else: if isinstance(value, list): @@ -429,6 +433,7 @@ class BaseClass(Base): value = value[0] else: raise ValueError("Object is too long to parse a single value.") + # value = value return super().__setattr__(key, value) case _: return super().__setattr__(key, value) diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index d007f61..1e7168d 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -3,6 +3,7 @@ All kit and reagent related models """ from __future__ import annotations import json, zipfile, yaml, logging, re +import sys from pprint import pformat from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB from sqlalchemy.orm import relationship, validates, Query @@ -231,6 +232,7 @@ class KitType(BaseClass): @classmethod def query_or_create(cls, **kwargs) -> Tuple[KitType, bool]: from backend.validators.pydant import PydKitType + from backend.validators.omni_gui_objects import BaseOmni new = False disallowed = ['expiry'] sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} @@ -1074,7 +1076,7 @@ class SubmissionType(BaseClass): new = True for k, v in sanitized_kwargs.items(): setattr(instance, k, v) - logger.info(f"Instance from query or create: {instance}") + logger.info(f"Instance from submissiontype query or create: {instance}") return instance, new @classmethod @@ -1298,7 +1300,7 @@ class SubmissionTypeKitTypeAssociation(BaseClass): new = True for k, v in sanitized_kwargs.items(): setattr(instance, k, v) - logger.info(f"Instance from query or create: {instance}") + logger.info(f"Instance from SubmissionTypeKitTypeAssociation query or create: {instance}") return instance, new @classmethod @@ -1428,6 +1430,22 @@ class KitTypeReagentRoleAssociation(BaseClass): except AttributeError: return "Blank KitTypeReagentRole" + @hybrid_property + def submissiontype(self): + return self.submission_type + + @submissiontype.setter + def submissiontype(self, value): + self.submission_type = value + + @hybrid_property + def kittype(self): + return self.kit_type + + @kittype.setter + def kittype(self, value): + self.kit_type = value + @validates('required') def validate_required(self, key, value): """ @@ -1478,8 +1496,31 @@ class KitTypeReagentRoleAssociation(BaseClass): instance = cls() new = True for k, v in sanitized_kwargs.items(): + logger.debug(f"Key: {k} has value: {v}") + match k: + case "kittype" | "kit_type": + k = "kit_type" + if isinstance(v, str): + v = KitType.query(name=v) + else: + v = v.instance_object + case "submissiontype" | "submission_type": + k = "submission_type" + if isinstance(v, str): + v = SubmissionType.query(name=v) + else: + v = v.instance_object + case "reagentrole" | "reagent_role": + k = "reagent_role" + if isinstance(v, str): + v = ReagentRole.query(name=v) + else: + v = v.instance_object + case _: + pass setattr(instance, k, v) - logger.info(f"Instance from query or create: {instance}") + logger.info(f"Instance from query or create: {instance.__dict__}") + # sys.exit() return instance, new @classmethod diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index e612680..925484b 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -460,6 +460,7 @@ class BasicSubmission(BaseClass, LogMixin): """ rows = range(1, plate_rows + 1) columns = range(1, plate_columns + 1) + logger.debug(f"sample list for plate map: {pformat(sample_list)}") # NOTE: An overly complicated list comprehension create a list of sample locations # NOTE: next will return a blank cell if no value found for row/column output_samples = [next((item for item in sample_list if item['row'] == row and item['column'] == column), @@ -1536,6 +1537,7 @@ class Wastewater(BasicSubmission): continue thing['tooltip'] = f"Sample Name: {thing['name']}\nWell: {thing['sample_location']}" dummy_samples.append(thing) + logger.debug(f"Dummy samples for 24 well: {pformat(dummy_samples)}") output['origin_plate'] = self.__class__.make_plate_map(sample_list=dummy_samples, plate_rows=4, plate_columns=6) # logger.debug(f"PCR info: {output['pcr_info']}") diff --git a/src/submissions/backend/validators/omni_gui_objects.py b/src/submissions/backend/validators/omni_gui_objects.py new file mode 100644 index 0000000..afda72f --- /dev/null +++ b/src/submissions/backend/validators/omni_gui_objects.py @@ -0,0 +1,579 @@ +from __future__ import annotations +import logging +import sys + +from pydantic import BaseModel, field_validator, Field +from typing import List, ClassVar +from backend.db.models import * +from sqlalchemy.orm.properties import ColumnProperty +from sqlalchemy.orm.relationships import _RelationshipDeclared + +logger = logging.getLogger(f"submissions.{__name__}") + + +class BaseOmni(BaseModel): + + instance_object: Any | None = Field(default=None) + + def __repr__(self): + try: + return f"<{self.__class__.__name__}({self.name})>" + except AttributeError: + return f"<{self.__class__.__name__}(NO NAME)>" + + @classproperty + def aliases(cls): + return cls.class_object.aliases + + def check_all_attributes(self, attributes: dict) -> bool: + """ + Checks this instance against a dictionary of attributes to determine if they are a match. + + Args: + attributes (dict): A dictionary of attributes to be check for equivalence + + Returns: + bool: If a single unequivocal value is found will be false, else true. + """ + logger.debug(f"Incoming attributes: {attributes}") + for key, value in attributes.items(): + # print(getattr(self.__class__, key).property) + if value.lower() == "none": + value = None + logger.debug(f"Attempting to grab attribute: {key}") + self_value = getattr(self, key) + class_attr = getattr(self.class_object, key) + # logger.debug(f"Self value: {self_value}, class attr: {class_attr} of type: {type(class_attr)}") + if isinstance(class_attr, property): + filter = "property" + else: + filter = class_attr.property + match filter: + case ColumnProperty(): + match class_attr.type: + case INTEGER(): + if value.lower() == "true": + value = 1 + elif value.lower() == "false": + value = 0 + else: + value = int(value) + case FLOAT(): + value = float(value) + case "property": + pass + case _RelationshipDeclared(): + logger.debug(f"Checking {self_value}") + try: + self_value = self_value.name + except AttributeError: + pass + if class_attr.property.uselist: + self_value = self_value.__str__() + try: + logger.debug(f"Check if {self_value.__class__} is subclass of {self.__class__}") + check = issubclass(self_value.__class__, self.__class__) + except TypeError as e: + logger.error(f"Couldn't check if {self_value.__class__} is subclass of {self.__class__} due to {e}") + check = False + if check: + logger.debug(f"Checking for subclass name.") + self_value = self_value.name + logger.debug( + f"Checking self_value {self_value} of type {type(self_value)} against attribute {value} of type {type(value)}") + if self_value != value: + output = False + logger.debug(f"Value {key} is False, returning.") + return output + return True + + def __setattr__(self, key, value): + try: + class_value = getattr(self.class_object, key) + except AttributeError: + return super().__setattr__(key, value) + try: + new_key = class_value.impl.key + except AttributeError: + new_key = None + logger.debug(f"Class value before new key: {class_value.property}") + if new_key and new_key != key: + class_value = getattr(self.class_object, new_key) + logger.debug(f"Class value after new key: {class_value.property}") + if isinstance(class_value, InstrumentedAttribute): + logger.debug(f"{key} is an InstrumentedAttribute with class_value.property: {class_value.property}.") + match class_value.property: + case ColumnProperty(): + logger.debug(f"Setting ColumnProperty to {value}") + return super().__setattr__(key, value) + case _RelationshipDeclared(): + logger.debug(f" {self.__class__.__name__} Setting _RelationshipDeclared for {key} to {value}") + if class_value.property.uselist: + logger.debug(f"Setting {key} with uselist") + existing = self.__getattribute__(key) + if existing is not None: + # NOTE: Getting some really weird duplicates for OmniSubmissionTypeKitTypeAssociation here. + logger.debug(f"Existing: {existing}, incoming: {value}") + if isinstance(value, list): + if value != existing: + value = existing + value + else: + value = existing + else: + if issubclass(value.__class__, self.__class__): + value = value.to_sql() + value = existing + [value] + else: + if issubclass(value.__class__, self.__class__): + value = value.to_sql() + value = [value] + # value = list(set(value)) + logger.debug(f"Final value for {key}: {value}") + return super().__setattr__(key, 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) + case _: + return super().__setattr__(key, value) + else: + return super().__setattr__(key, value) + + +class OmniSubmissionType(BaseOmni): + class_object: ClassVar[Any] = SubmissionType + + name: str = Field(default="", description="property") + info_map: dict = Field(default={}, description="property") + defaults: dict = Field(default={}, description="property") + template_file: bytes = Field(default=bytes(), description="property") + sample_map: dict = Field(default={}, description="property") + + @field_validator("name", mode="before") + @classmethod + def rescue_name_none(cls, value): + if not value: + return "" + return value + + @field_validator("sample_map", mode="before") + @classmethod + def rescue_sample_map_none(cls, value): + if not value: + return {} + return value + + @field_validator("defaults", mode="before") + @classmethod + def rescue_defaults_none(cls, value): + if not value: + return {} + return value + + @field_validator("info_map", mode="before") + @classmethod + def rescue_info_map_none(cls, value): + if not value: + return {} + return value + + def __init__(self, instance_object: Any, **data): + super().__init__(**data) + self.instance_object = instance_object + + def to_dataframe_dict(self): + return dict( + name=self.name + ) + + def to_sql(self): + instance, new = self.class_object.query_or_create(name=self.name) + instance.info_map = self.info_map + instance.defaults = self.defaults + instance.sample_map = self.sample_map + if self.template_file: + instance.template_file = self.template_file + return instance + + +class OmniReagentRole(BaseOmni): + class_object: ClassVar[Any] = ReagentRole + + name: str = Field(default="", description="property") + eol_ext: timedelta = Field(default=timedelta(days=0), description="property") + + @field_validator("name", mode="before") + @classmethod + def rescue_name_none(cls, value): + if not value: + return "" + return value + + @field_validator("eol_ext", mode="before") + @classmethod + def rescue_eol_ext(cls, value): + if not value: + value = timedelta(days=0) + return value + + def __init__(self, instance_object: Any, **data): + super().__init__(**data) + self.instance_object = instance_object + + def to_dataframe_dict(self): + return dict( + name=self.name + ) + + def to_sql(self): + instance, new = self.class_object.query_or_create(name=self.name) + if new: + instance.eol_ext = self.eol_ext + return instance + + +class OmniSubmissionTypeKitTypeAssociation(BaseOmni): + class_object: ClassVar[Any] = SubmissionTypeKitTypeAssociation + + submissiontype: str | OmniSubmissionType = Field(default="", description="relationship", title="SubmissionType") + kittype: str | OmniKitType = Field(default="", description="relationship", title="KitType") + mutable_cost_column: float = Field(default=0.0, description="property") + mutable_cost_sample: float = Field(default=0.0, description="property") + constant_cost: float = Field(default=0.0, description="property") + # processes: List[OmniProcess] | List[str] = Field(default=[], description="relationship", title="Process") + + def __repr__(self): + if isinstance(self.submissiontype, str): + submissiontype = self.submissiontype + else: + submissiontype = self.submissiontype.name + if isinstance(self.kittype, str): + kittype = self.kittype + else: + kittype = self.kittype.name + try: + return f"<{self.__class__.__name__}({submissiontype}&{kittype})>" + except AttributeError: + return f"<{self.__class__.__name__}(NO NAME)>" + + @field_validator("submissiontype", mode="before") + @classmethod + def rescue_submissiontype_none(cls, value): + if not value: + return "" + return value + + @field_validator("kittype", mode="before") + @classmethod + def rescue_kittype_none(cls, value): + if not value: + return "" + return value + + @field_validator("kittype") + @classmethod + def no_list_please(cls, value): + if isinstance(value, list): + raise ValueError("List is not allowed for kittype.") + return value + + def __init__(self, instance_object: Any, **data): + super().__init__(**data) + self.instance_object = instance_object + + def to_dataframe_dict(self): + if isinstance(self.submissiontype, OmniSubmissionType): + submissiontype = self.submissiontype.name + else: + submissiontype = self.submissiontype + if isinstance(self.kittype, OmniKitType): + kittype = self.kittype.name + else: + kittype = self.kittype + return dict( + submissiontype=submissiontype, + kittype=kittype, + mutable_cost_column=self.mutable_cost_column, + mutable_cost_sample=self.mutable_cost_sample, + constant_cost=self.constant_cost + ) + + + def to_sql(self): + logger.debug(f"Self kittype: {self.submissiontype}") + if issubclass(self.submissiontype.__class__, BaseOmni): + submissiontype = SubmissionType.query(name=self.submissiontype.name) + else: + submissiontype = SubmissionType.query(name=self.submissiontype) + if issubclass(self.kittype.__class__, BaseOmni): + kittype = KitType.query(name=self.kittype.name) + else: + kittype = KitType.query(name=self.kittype) + # logger.debug(f"Self kittype: {self.kittype}") + # kittype = KitType.query(name=self.kittype) + logger.debug(f"Query or create with {kittype}, {submissiontype}") + instance, is_new = self.class_object.query_or_create(kittype=kittype, submissiontype=submissiontype) + instance.mutable_cost_column = self.mutable_cost_column + instance.mutable_cost_sample = self.mutable_cost_sample + instance.constant_cost = self.constant_cost + return instance + + +class OmniKitTypeReagentRoleAssociation(BaseOmni): + class_object: ClassVar[Any] = KitTypeReagentRoleAssociation + + reagent_role: str | OmniReagentRole = Field(default="", description="relationship", title="ReagentRole") + uses: dict = Field(default={}, description="property") + required: bool = Field(default=True, description="property") + submission_type: str | OmniSubmissionType = Field(default="", description="relationship", title="SubmissionType") + kit_type: str | OmniKitType = Field(default="", description="relationship", title="KitType") + + + @field_validator("uses", mode="before") + @classmethod + def rescue_uses_none(cls, value): + if not value: + return {} + return value + + def __init__(self, instance_object: Any, **data): + super().__init__(**data) + self.instance_object = instance_object + + def to_dataframe_dict(self): + if isinstance(self.submission_type, OmniSubmissionType): + submission_type = self.submission_type.name + else: + submission_type = self.submission_type + if isinstance(self.kit_type, OmniKitType): + kit_type = self.kit_type.name + else: + kit_type = self.kit_type + # name = f"{kit_type} -> {self.reagent_role}" + # logger.debug(f"Using name: {name}") + if isinstance(self.reagent_role, OmniReagentRole): + reagent_role = self.reagent_role.name + else: + reagent_role = self.reagent_role + return dict( + reagent_role=reagent_role, + # name=self.reagent_role.name, + submission_type=submission_type, + kit_type=kit_type + ) + + def to_sql(self): + if isinstance(self.reagent_role, OmniReagentRole): + reagent_role = self.reagent_role.name + else: + reagent_role = self.reagent_role + instance, new = self.class_object.query_or_create( + reagentrole=reagent_role, + kittype=self.kit_type, + submissiontype=self.submission_type + ) + if new: + reagent_role = self.reagent_role.to_sql() + instance.reagent_role = reagent_role + logger.debug(f"KTRRAssoc uses: {self.uses}") + instance.uses = self.uses + logger.debug(f"KitTypeReagentRoleAssociation: {pformat(instance.__dict__)}") + + return instance + + +class OmniEquipmentRole(BaseOmni): + class_object: ClassVar[Any] = EquipmentRole + + name: str = Field(default="", description="property") + + @field_validator("name", mode="before") + @classmethod + def rescue_name_none(cls, value): + if not value: + return "" + return value + + def __init__(self, instance_object: Any, **data): + super().__init__(**data) + self.instance_object = instance_object + + def to_dataframe_dict(self): + return dict( + name=self.name + ) + + def to_sql(self): + instance, new = self.class_object.query_or_create(name=self.name) + return instance + + +class OmniTips(BaseOmni): + class_object: ClassVar[Any] = Tips + + name: str = Field(default="", description="property") + + @field_validator("name", mode="before") + @classmethod + def rescue_name_none(cls, value): + if not value: + return "" + return value + + def __init__(self, instance_object: Any, **data): + super().__init__(**data) + self.instance_object = instance_object + + def to_dataframe_dict(self): + return dict( + name=self.name + ) + + def to_sql(self): + instance, new = self.class_object.query_or_create(name=self.name) + return instance + + +class OmniTipRole(BaseOmni): + class_object: ClassVar[Any] = TipRole + + name: str = Field(default="", description="property") + tips: List[OmniTips] = Field(default=[], description="relationship", title="Tips") + + @field_validator("name", mode="before") + @classmethod + def rescue_name_none(cls, value): + if not value: + return "" + return value + + def __init__(self, instance_object: Any, **data): + super().__init__(**data) + self.instance_object = instance_object + + def to_dataframe_dict(self): + return dict( + name=self.name, + tips=[item.name for item in self.tips] + ) + + def to_sql(self): + instance, new = self.class_object.query_or_create(name=self.name) + for tips in self.tips: + tips.to_sql() + # if new_assoc not in instance.instances: + # instance.instances.append(new_assoc) + return instance + + +class OmniProcess(BaseOmni): + class_object: ClassVar[Any] = Process + + # NOTE: How am I going to figure out relatioinships without getting into recursion issues? + name: str = Field(default="", description="property") #: Process name + submission_types: List[OmniSubmissionType] | List[str] = Field(default=[], description="relationship", + title="SubmissionType") + equipment_roles: List[OmniEquipmentRole] | List[str] = Field(default=[], description="relationship", + title="EquipmentRole") + tip_roles: List[OmniTipRole] | List[str] = Field(default=[], description="relationship", title="TipRole") + + def __init__(self, instance_object: Any, **data): + super().__init__(**data) + self.instance_object = instance_object + + def to_dataframe_dict(self): + submissiontypes = [item.name for item in self.submission_types] + logger.debug(f"Submission Types: {submissiontypes}") + equipmentroles = [item.name for item in self.equipment_roles] + logger.debug(f"Equipment Roles: {equipmentroles}") + return dict( + name=self.name, + submission_types=submissiontypes, + equipment_roles=equipmentroles + ) + + @field_validator("name", mode="before") + @classmethod + def rescue_name_none(cls, value): + if not value: + return "" + return value + + def to_sql(self): + instance, new = self.class_object.query_or_create(name=self.name) + for st in self.submission_types: + new_assoc = st.to_sql() + if new_assoc not in instance.submission_types: + instance.submission_types.append(new_assoc) + for er in self.equipment_roles: + new_assoc = er.to_sql() + if new_assoc not in instance.equipment_roles: + instance.equipment_roles.append(new_assoc) + for tr in self.tip_roles: + new_assoc = tr.to_sql() + if new_assoc not in instance.tip_roles: + instance.tip_roles.append(new_assoc) + return instance + + +class OmniKitType(BaseOmni): + class_object: ClassVar[Any] = KitType + + name: str = Field(default="", description="property") + kit_submissiontype_associations: List[OmniSubmissionTypeKitTypeAssociation] | List[str] = Field(default=[], + description="relationship", + title="SubmissionTypeKitTypeAssociation") + kit_reagentrole_associations: List[OmniKitTypeReagentRoleAssociation] | List[str] = Field(default=[], + description="relationship", + title="KitTypeReagentRoleAssociation") + processes: List[OmniProcess] | List[str] = Field(default=[], description="relationship", title="Process") + + @field_validator("name", mode="before") + @classmethod + def rescue_name_none(cls, value): + if not value: + return "" + return value + + def __init__(self, instance_object: Any, **data): + super().__init__(**data) + self.instance_object = instance_object + + def to_dataframe_dict(self): + return dict( + name=self.name + ) + + def to_sql(self) -> KitType: + kit, is_new = KitType.query_or_create(name=self.name) + if is_new: + logger.debug(f"New kit made: {kit}") + else: + logger.debug(f"Kit retrieved: {kit}") + new_rr = [] + for rr_assoc in self.kit_reagentrole_associations: + new_assoc = rr_assoc.to_sql() + if new_assoc not in new_rr: + logger.debug(f"Adding {new_assoc} to kit_reagentrole_associations") + new_rr.append(new_assoc) + logger.debug(f"Setting kit_reagentrole_associations to {new_rr}") + kit.kit_reagentrole_associations = new_rr + new_st = [] + for st_assoc in self.kit_submissiontype_associations: + new_assoc = st_assoc.to_sql() + if new_assoc not in new_st: + new_st.append(new_assoc) + kit.kit_submissiontype_associations = new_st + new_processes = [] + for process in self.processes: + new_process = process.to_sql() + if new_process not in new_processes: + new_processes.append(new_process) + kit.processes = new_processes + logger.debug(f"Kit: {pformat(kit.__dict__)}") + for item in kit.kit_reagentrole_associations: + logger.debug(f"KTRRassoc: {item.__dict__}") + return kit diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index 198e3da..d5aaead 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -242,6 +242,7 @@ class App(QMainWindow): from frontend.widgets.omni_manager_pydant import ManagerWindow as ManagerWindowPyd dlg = ManagerWindowPyd(parent=self, object_type=KitType, extras=[], add_edit='edit', managers=set()) if dlg.exec(): + logger.debug("\n\nBeginning parsing\n\n") output = dlg.parse_form() # assert isinstance(output, KitType) # output.save() @@ -249,6 +250,11 @@ class App(QMainWindow): # output.to_sql() with open(f"{output.name}.obj", "wb") as f: pickle.dump(output, f) + logger.debug("\n\nBeginning transformation\n\n") + sql = output.to_sql() + with open(f"{output.name}.sql", "wb") as f: + pickle.dump(sql, f) + sql.save() class AddSubForm(QWidget): diff --git a/src/submissions/frontend/widgets/omni_manager_pydant.py b/src/submissions/frontend/widgets/omni_manager_pydant.py new file mode 100644 index 0000000..a41c2b6 --- /dev/null +++ b/src/submissions/frontend/widgets/omni_manager_pydant.py @@ -0,0 +1,680 @@ +""" +Provides a screen for managing all attributes of a database object. +""" +import json, logging +import sys +from json.decoder import JSONDecodeError +from datetime import datetime, timedelta +from pprint import pformat +from typing import Any, List, Literal +from PyQt6.QtCore import QSortFilterProxyModel, Qt +from PyQt6.QtGui import QAction, QCursor +from PyQt6.QtWidgets import ( + QLabel, QDialog, + QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit, QMenu, + QDoubleSpinBox, QSpinBox, QCheckBox, QTextEdit, QVBoxLayout, QHBoxLayout +) +from pandas import DataFrame +from backend import db +from tools import check_object_in_manager +from .omni_search import SearchBox +from frontend.widgets.submission_table import pandasModel + +logger = logging.getLogger(f"submissions.{__name__}") + + +class ManagerWindow(QDialog): + """ + Initially this is a window to manage Organization Contacts, but hope to abstract it more later. + """ + + def __init__(self, parent, + extras: List[str], + instance: Any | None = None, + object_type: Any | None = None, + manager: Any | None = None, + add_edit: Literal['add', 'edit'] = 'edit', + **kwargs): + super().__init__(parent) + # NOTE: Should I pass in an instance? + self.instance = instance + logger.debug(f"Setting instance: {self.instance}") + if not self.instance: + self.class_object = self.original_type = object_type + else: + self.class_object = self.original_type = self.instance.__class__ + self.add_edit = add_edit + if manager is None: + try: + self.manager = self.parent().omni_object + except AttributeError: + self.manager = None + else: + self.manager = manager + # logger.debug(f"Manager: {manager}") + self.extras = extras + self.context = kwargs + self.layout = QGridLayout(self) + QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + self.setMinimumSize(600, 600) + sub_classes = ["Any"] + [cls.__name__ for cls in self.class_object.__subclasses__()] + if len(sub_classes) > 1: + self.sub_class = QComboBox(self) + self.sub_class.setObjectName("sub_class") + self.sub_class.addItems(sub_classes) + self.sub_class.currentTextChanged.connect(self.update_options) + self.sub_class.setEditable(False) + self.sub_class.setMinimumWidth(self.minimumWidth()) + self.layout.addWidget(self.sub_class, 0, 0) + else: + self.sub_class = None + self.update_instance(initial=True) + if self.add_edit == "edit": + self.options = QComboBox(self) + self.options.setObjectName("options") + self.update_options() + else: + self.update_data() + self.setLayout(self.layout) + self.setWindowTitle(f"Manage {self.class_object.__name__} - Manager: {self.manager}") + + def update_options(self) -> None: + """ + Changes form inputs based on sample type + """ + # logger.debug(f"Instance: {self.instance}") + if self.sub_class: + self.class_object = getattr(db, self.sub_class.currentText()) + # logger.debug(f"From update options, managers: {self.managers}") + try: + query_kwargs = {self.parent().instance.query_alias: self.parent().instance} + except AttributeError as e: + # logger.debug(f"Couldn't set query kwargs due to: {e}") + query_kwargs = {} + # logger.debug(f"Query kwargs: {query_kwargs}") + # logger.debug(f"self.class_object: {self.class_object}") + options = [item.name for item in self.class_object.query(**query_kwargs)] + if self.instance: + try: + inserter = options.pop(options.index(self.instance.name)) + except ValueError: + inserter = self.instance.name + options.insert(0, inserter) + self.options.clear() + self.options.addItems(options) + self.options.setEditable(False) + self.options.setMinimumWidth(self.minimumWidth()) + self.layout.addWidget(self.options, 1, 0, 1, 1) + # if len(options) > 0: + self.add_button = QPushButton("Add New") + self.layout.addWidget(self.add_button, 1, 1, 1, 1) + self.add_button.clicked.connect(self.add_new) + self.options.currentTextChanged.connect(self.update_instance) + # logger.debug(f"Instance: {self.instance}") + self.update_data() + + def update_instance(self, initial: bool = False): + if self.add_edit == "edit" or initial: + try: + # logger.debug(f"Querying with {self.options.currentText()}") + self.instance = self.class_object.query(name=self.options.currentText(), limit=1) + except AttributeError: + # self.instance = None + pass + # logger.debug(f"Instance: {self.instance}") + if not self.instance: + logger.warning(f"Instance not found, creating blank instance.") + self.instance = self.class_object() + # logger.debug(f"self.instance: {self.instance}") + if issubclass(self.instance.__class__, db.BaseClass): + self.omni_object = self.instance.to_omni(expand=True) + else: + self.omni_object = self.instance + logger.debug(f"Created omni_object: {self.omni_object.__dict__}") + self.update_data() + + def update_data(self) -> None: + """ + Performs updating of widgets on first run and after options change. + + Returns: + None + """ + # NOTE: Remove all old widgets. + deletes = [item for item in self.findChildren(EditProperty)] + \ + [item for item in self.findChildren(EditRelationship)] + \ + [item for item in self.findChildren(QDialogButtonBox)] + for item in deletes: + item.setParent(None) + fields = self.omni_object.__class__.model_fields + for key, info in fields.items(): + # logger.debug(f"Attempting to set {key}, {info} widget") + try: + value = getattr(self.omni_object, key) + except AttributeError: + value = None + match info.description: + # NOTE: ColumnProperties will be directly edited. + case "property": + # NOTE: field.property.expression.type gives db column type eg. STRING or TIMESTAMP + # logger.debug(f"Creating property widget with value: {value}") + widget = EditProperty(self, key=key, column_type=info, value=value) + # NOTE: RelationshipDeclareds will be given a list of existing related objects. + case "relationship": + # NOTE: field.comparator.class_object.class_ gives the relationship class + # logger.debug(f"Creating relationship widget with value: {value}") + widget = EditRelationship(self, key=key, class_object=info.title, value=value) + case _: + continue + if widget: + self.layout.addWidget(widget, self.layout.rowCount(), 0, 1, 2) + # NOTE: Add OK|Cancel to bottom of dialog. + self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0, 1, 2) + + def parse_form(self) -> Any: + """ + Returns the instance associated with this window. + + Returns: + Any: The instance with updated fields. + """ + # TODO: Need Relationship property here too? + results = [item.parse_form() for item in self.findChildren(EditProperty)] + for result in results: + logger.debug(f"Incoming property result: {result}") + setattr(self.omni_object, result['field'], result['value']) + # NOTE: Getting 'None' back here. + 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 relationship result: {result}") + # if not getattr(self.omni_object, result['field']): + setattr(self.omni_object, result['field'], result['value']) + # logger.debug(f"Set result: {getattr(self.omni_object, result['field'])}") + # logger.debug(f"Instance coming from parsed form: {self.omni_object.__dict__}") + return self.omni_object + + def add_new(self): + new_instance = self.class_object() + self.instance = new_instance + self.update_options() + + +class EditProperty(QWidget): + + def __init__(self, parent: ManagerWindow, key: str, column_type: Any, value): + super().__init__(parent) + self.label = QLabel(key.title().replace("_", " ")) + self.layout = QGridLayout() + self.layout.addWidget(self.label, 0, 0, 1, 1) + self.setObjectName(key) + # logger.debug(f"Column type for {key}: {type(column_type.default)}") + match column_type.default: + case str(): + self.widget = QLineEdit(self) + self.widget.setText(value) + case bool(): + if isinstance(column_type.default, bool): + self.widget = QCheckBox() + self.widget.setChecked(value) + else: + if value is None: + value = 0 + self.widget = QSpinBox() + self.widget.setMaximum(1) + self.widget.setValue(value) + case float(): + if not value: + value = 0.0 + self.widget = QDoubleSpinBox() + self.widget.setMaximum(999.99) + self.widget.setValue(value) + case datetime(): + self.widget = QDateEdit(self) + self.widget.setDate(value) + case timedelta(): + self.widget = QSpinBox() + self.widget.setMaximum(9999) + self.widget.setToolTip("This time interval is measured in days.") + self.widget.setValue(value.days) + case dict(): + self.widget = JsonEditButton(parent=self, key=key, value=value) + # self.widget.viewButton.clicked.connect(lambda: self.parent().toggle_textedit(self.widget)) + # self.widget.addButton.clicked.connect(lambda: self.parent().add_to_json(self.widget)) + case bytes(): + self.widget = QLabel("BLOB Under construction") + case _: + self.widget = None + self.layout.addWidget(self.widget, 0, 1, 1, 3) + self.setLayout(self.layout) + + def parse_form(self): + # logger.debug(f"Parsing widget {self.objectName()}: {type(self.widget)}") + match self.widget: + case QLineEdit(): + value = self.widget.text() + case QDateEdit(): + value = self.widget.date() + case QSpinBox() | QDoubleSpinBox(): + value = self.widget.value() + case QCheckBox(): + value = self.widget.isChecked() + case JsonEditButton(): + value = self.widget.data + case _: + value = None + return dict(field=self.objectName(), value=value) + + +class EditRelationship(QWidget): + + def __init__(self, parent, key: str, class_object: Any, value): + from backend.db import models + super().__init__(parent) + self.class_object = getattr(models, class_object) + logger.debug(f"Class object: {self.class_object}") + self.setParent(parent) + # logger.debug(f"Edit relationship class_object: {self.class_object}") + self.label = QLabel(key.title().replace("_", " ")) + self.setObjectName(key) #: key is the name of the relationship this represents + logger.debug(f"Checking relationship for {self.parent().class_object}: {key}") + # try: + # self.relationship = getattr(self.parent().instance.__class__, key) + self.relationship = getattr(self.parent().class_object, key) + #: relationship object for type differentiation + # except AttributeError: + # logger.warning(f"Could not get relationship for: {key}.") + # self.relationship = None + self.widget = QTableView() + self.add_button = QPushButton("Add New") + self.add_button.clicked.connect(self.add_new) + self.existing_button = QPushButton("Add Existing") + self.existing_button.clicked.connect(self.add_existing) + # self.existing_button.setEnabled(self.class_object.level == 1) + if not isinstance(value, list): + if value not in [None, ""]: + value = [value] + else: + value = [] + self.data = value + logger.debug(f"Set data: {self.data}") + # self.update_buttons() + logger.debug(f"Parent manager: {self.parent().manager}") + checked_manager, is_primary = check_object_in_manager(self.parent().manager, self.objectName()) + if checked_manager: + logger.debug(f"Checked manager for {self.objectName()}: {checked_manager}") + # logger.debug(f"Omni will inherit: {self.class_object.omni_inheritable} from {self.parent().class_object}") + # if checked_manager is not None:# and not self.data:# and self.objectName() in self.parent().class_object.omni_inheritable: + # # logger.debug(f"Setting {checked_manager} in self.data") + # # if isinstance(checked_manager, InstrumentedList): + # # self.data = [item.to_omni() for item in checked_manager] + # # else: + # # self.data = [checked_manager.to_omni()] + # self.data = [checked_manager] + if not self.data: + self.data = [checked_manager] + try: + logger.debug(f"Relationship {key} uses list: {self.relationship.property.uselist}") + check = not self.relationship.property.uselist and len(self.data) >= 1 + except AttributeError: + check = True + if check: + self.add_button.setEnabled(False) + self.existing_button.setEnabled(False) + if is_primary: + self.widget.setEnabled(False) + else: + self.add_button.setEnabled(True) + self.existing_button.setEnabled(True) + if is_primary: + self.widget.setEnabled(True) + self.layout = QGridLayout() + self.layout.addWidget(self.label, 0, 0, 1, 5) + 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.existing_button, 0, 7, 1, 1, alignment=Qt.AlignmentFlag.AlignRight) + self.setLayout(self.layout) + self.set_data() + + def update_buttons(self): + if not self.relationship.property.uselist and len(self.data) >= 1: + # logger.debug(f"Property {self.relationship} doesn't use list and data is of length: {len(self.data)}") + self.add_button.setEnabled(False) + self.existing_button.setEnabled(False) + else: + self.add_button.setEnabled(True) + self.existing_button.setEnabled(True) + + def parse_row(self, x): + context = {item: x.sibling(x.row(), self.df.columns.get_loc(item)).data() for item in self.df.columns} + try: + object = self.class_object.query(**context) + except KeyError: + object = None + self.widget.doubleClicked.disconnect() + self.add_edit(instance=object) + + def add_new(self, instance: Any = None, add_edit: Literal["add", "edit"] = "add"): + if add_edit == "edit": + logger.debug(f"Editing instance: {instance.__dict__}") + # NOTE: if an existing instance is not being edited, create a new instance + if not instance: + # logger.debug(f"Creating new instance of {self.class_object}") + instance = self.class_object() + # logger.debug(f"Creating manager window for {instance}") + manager = self.parent().manager + # logger.debug(f"Managers going into add new: {managers}") + # dlg = ManagerWindow(self.parent(), object_type=instance.__class__, extras=[], manager=manager, add_edit="add") + dlg = ManagerWindow(self.parent(), instance=instance, extras=[], manager=manager, add_edit=add_edit) + if dlg.exec(): + new_instance = dlg.parse_form() + logger.debug(f"New instance: {pformat(new_instance.__dict__)}") + # NOTE: Somewhere between this and the next logger, I'm losing the uses data. + if add_edit == "add": + self.parent().omni_object.__setattr__(self.objectName(), new_instance) + else: + instance.__dict__.update(new_instance.__dict__) + logger.debug(f"Final instance: {pformat(instance.__dict__)}") + # self.parent().instance.save() + self.parent().update_data() + + def add_existing(self): + dlg = SearchBox(self, object_type=self.class_object, returnable=True, extras=[]) + if dlg.exec(): + rows = dlg.return_selected_rows() + for row in rows: + # logger.debug(f"Querying with {row}") + instance = self.class_object.query(**row) + # NOTE: My custom __setattr__ should take care of any list problems. + if isinstance(instance, list): + instance = instance[0] + self.parent().omni_object.__setattr__(self.objectName(), instance.to_omni()) + # self.parent().instance.save() + self.parent().update_data() + + def set_data(self) -> None: + """ + sets data in model + """ + # logger.debug(f"Self.data: {self.data}") + try: + # records = [{k: v['instance_attr'] for k, v in item.omnigui_instance_dict.items()} for item in self.data] + records = [item.to_dataframe_dict() 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): + self.columns_of_interest = [] + try: + self.df['id'] = self.df['id'].apply(str) + self.df['id'] = self.df['id'].str.zfill(4) + except KeyError as e: + logger.error(f"Could not alter id to string due to KeyError: {e}") + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(pandasModel(self.df)) + self.widget.setModel(proxy_model) + self.widget.resizeColumnsToContents() + self.widget.resizeRowsToContents() + self.widget.setSortingEnabled(True) + self.widget.doubleClicked.connect(self.parse_row) + # self.update_buttons() + + def contextMenuEvent(self, event): + """ + Creates actions for right click menu events. + + Args: + event (_type_): the item of interest + """ + if not self.widget.isEnabled(): + logger.warning(f"{self.objectName()} is disabled.") + return + id = self.widget.selectionModel().currentIndex() + logger.debug(f"Row id: {id.row()}") + # NOTE: the overly complicated {column_name: row_value} dictionary construction + row_data = {self.df.columns[column]: self.widget.model().index(id.row(), column).data() for column in + range(self.widget.model().columnCount())} + # logger.debug(f"Row data: {row_data}") + logger.debug(f"Attempting to grab {self.objectName()} from {self.parent().omni_object}") + object = getattr(self.parent().omni_object, self.objectName()) + logger.debug(f"Initial object: {object}") + if isinstance(object, list): + try: + object = next((item for item in object if item.check_all_attributes(attributes=row_data))) + except StopIteration: + logger.warning(f"Failed to find all attributes equal, getting row {id.row()}") + object = object[id.row()] + object.instance_object = object.to_sql() + logger.debug(f"Object of interest: {pformat(object.__dict__)}") + self.menu = QMenu(self) + try: + remove_action = QAction(f"Remove {object.name}", self) + except AttributeError: + remove_action = QAction(f"Remove object", self) + remove_action.triggered.connect(lambda: self.remove_item(object=object)) + self.menu.addAction(remove_action) + try: + edit_action = QAction(f"Edit {object.name}", self) + except AttributeError: + edit_action = QAction(f"Edit object", self) + edit_action.triggered.connect(lambda: self.add_new(instance=object.instance_object, add_edit="edit")) + self.menu.addAction(edit_action) + self.menu.popup(QCursor.pos()) + + def remove_item(self, object): + logger.debug(f"Attempting to remove {object} from {self.parent().instance.__dict__}") + # editor = getattr(self.parent().instance, self.objectName().lower()) + editor = getattr(self.parent().omni_object, self.objectName().lower()) + logger.debug(f"Editor: {editor}") + # if object == self.parent().manager: + # logger.error(f"Can't remove manager object.") + # return + # logger.debug(f"Object: {object}") + # try: + # self.data.remove(object) + # except (AttributeError, ValueError) as e: + # logger.error(f"Couldn't remove object from self.data due to: {e}") + # self.data = [] + try: + # logger.debug(f"Using remove technique") + editor.remove(object) + except AttributeError as e: + logger.error(f"Remove failed using set to None for {self.objectName().lower()}.") + setattr(self.parent().omni_object, self.objectName().lower(), None) + except ValueError as e: + logger.error(f"Remove failed for {self.objectName().lower()} due to {e}.") + logger.debug(f"Setting {self.objectName()} to {editor}") + setattr(self.parent().omni_object, self.objectName().lower(), editor) + logger.debug(f"After set: {getattr(self.parent().omni_object, self.objectName().lower())}") + # self.parent().instance.save() + # self.parent().update_data() + self.set_data() + self.update_buttons() + + def parse_form(self): + # logger.debug(f"Returning parsed form data from {self.objectName()}: {self.data}") + try: + check = self.relationship.property.uselist + except AttributeError: + check = False + if check and isinstance(self.data, list): + output_data = self.data[0] + else: + output_data = self.data + return dict(field=self.objectName(), value=output_data) + + +class JsonEditButton(QWidget): + + def __init__(self, parent, key: str, value: str = ""): + super().__init__(parent) + logger.debug(f"Setting jsonedit data to: {value}") + self.data = value + self.setParent(parent) + self.setObjectName(key) + self.addButton = QPushButton("Add Entry", parent=self) + self.addButton.clicked.connect(self.add_to_json) + self.viewButton = QPushButton("View >>>", parent=self) + self.viewButton.clicked.connect(self.toggle_textedit) + self.layout = QGridLayout() + self.layout.addWidget(self.addButton, 0, 0) + self.layout.addWidget(self.viewButton, 0, 1) + self.setLayout(self.layout) + self.edit_box = LargeTextEdit(parent=self, key=key) + self.parent().parent().layout.addWidget(self.edit_box, 1, self.parent().parent().layout.columnCount(), + self.parent().parent().layout.rowCount() - 1, 1) + self.edit_box.setVisible(False) + self.edit_box.widget.textChanged.connect(self.set_json_to_text) + + def set_json_to_text(self): + logger.debug(self.edit_box.widget.toPlainText()) + text = self.edit_box.widget.toPlainText() + try: + jsoner = json.loads(text) + except JSONDecodeError: + jsoner = None + if jsoner: + self.data = jsoner + + def add_to_json(self): + jsonedit = JsonEditScreen(parent=self, parameter=self.objectName()) + if jsonedit.exec(): + data = jsonedit.parse_form() + # logger.debug(f"Data: {pformat(data)}") + self.data = data + + def toggle_textedit(self): + self.edit_box.setVisible(not self.edit_box.isVisible()) + # data = getattr(self.omni_object, name) + # logger.debug(f"Data: {data}") + data = json.dumps(self.data, indent=4) + self.edit_box.widget.setText(data) + + +class JsonEditScreen(QDialog): + + def __init__(self, parent, parameter: str): + super().__init__(parent) + self.class_obj = parent.parent().parent().class_object + self.layout = QGridLayout() + # logger.debug(f"Parameter: {parameter}") + self.setWindowTitle(parameter) + try: + self.json_field = getattr(self.class_obj, f"{parameter}_json_edit_fields") + except AttributeError: + self.json_field = self.class_obj.json_edit_fields + match self.json_field: + case dict(): + for key, value in self.json_field.items(): + # logger.debug(f"Key: {key}, Value: {value}") + row = self.layout.rowCount() + self.layout.addWidget(QLabel(key), row, 0) + match value: + case "int": + self.widget = QSpinBox() + case "str": + self.widget = QLineEdit() + case dict(): + self.widget = DictionaryJsonSubEdit(parent=self, key=key, dic=value) + case _: + continue + self.widget.setObjectName(key) + self.layout.addWidget(self.widget, row, 1) + QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + self.layout.addWidget(self.buttonBox, self.layout.rowCount(), 0, 1, 2) + self.setLayout(self.layout) + + def parse_form(self): + widgets = [item for item in self.findChildren(QWidget) if item.objectName() in self.json_field.keys()] + # logger.debug(f"Widgets: {widgets}") + # logger.debug(type(self.json_field)) + if isinstance(self.json_field, dict): + output = {} + elif isinstance(self.json_field, list): + output = [] + else: + raise ValueError(f"Inappropriate data type: {type(self.json_field)}") + for widget in widgets: + # logger.debug(f"JsonEditScreen Widget: {widget}") + key = widget.objectName() + match widget: + case QSpinBox(): + value = widget.value() + case QLineEdit(): + value = widget.text() + case DictionaryJsonSubEdit(): + value = widget.parse_form() + case _: + continue + if isinstance(self.json_field, dict): + output[key] = value + elif isinstance(self.json_field, list): + if isinstance(value, list): + output += value + else: + output.append(value) + else: + raise ValueError(f"Inappropriate data type: {type(self.json_field)}") + # output[key] = value + return output + + +class DictionaryJsonSubEdit(QWidget): + + def __init__(self, parent, key, dic: dict): + super().__init__(parent) + self.layout = QHBoxLayout() + self.setObjectName(key) + self.data = dic + for key, value in self.data.items(): + self.layout.addWidget(QLabel(key)) + match value: + case "int": + self.widget = QSpinBox() + case "str": + self.widget = QLineEdit() + case dict(): + self.widget = DictionaryJsonSubEdit(parent, key=key, dic=value) + self.widget.setObjectName(key) + self.layout.addWidget(self.widget) + self.setLayout(self.layout) + + def parse_form(self): + widgets = [item for item in self.findChildren(QWidget) if item.objectName() in self.data.keys()] + # logger.debug(f"Widgets: {widgets}") + output = {} + for widget in widgets: + # logger.debug(f"DictionaryJsonSubEdit Widget: {widget}") + key = widget.objectName() + match widget: + case QSpinBox(): + value = widget.value() + case QLineEdit(): + value = widget.text() + case DictionaryJsonSubEdit(): + value = widget.parse_form() + case _: + continue + output[key] = value + return output + + +class LargeTextEdit(QWidget): + + def __init__(self, parent, key: str): + super().__init__(parent) + self.setParent(parent) + self.setObjectName(key) + self.widget = QTextEdit() + self.layout = QVBoxLayout() + self.layout.addWidget(self.widget) + self.setLayout(self.layout)