diff --git a/alembic.ini b/alembic.ini index 986a474..4833d54 100644 --- a/alembic.ini +++ b/alembic.ini @@ -55,8 +55,8 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db -; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\DB_backups\submissions-20230705.db +; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db +sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\DB_backups\submissions-20230712.db ; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions_test.db diff --git a/alembic/versions/78178df0286a_making_sample_ids_unique.py b/alembic/versions/78178df0286a_making_sample_ids_unique.py new file mode 100644 index 0000000..c5832d7 --- /dev/null +++ b/alembic/versions/78178df0286a_making_sample_ids_unique.py @@ -0,0 +1,38 @@ +"""making sample ids unique + +Revision ID: 78178df0286a +Revises: 4c6221f01324 +Create Date: 2023-07-26 13:55:41.864399 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '78178df0286a' +down_revision = '4c6221f01324' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('_bc_samples', schema=None) as batch_op: + batch_op.create_unique_constraint("unique_bc_sample", ['sample_id']) + + with op.batch_alter_table('_ww_samples', schema=None) as batch_op: + batch_op.create_unique_constraint("unique_ww_sample", ['ww_sample_full_id']) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('_ww_samples', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='unique') + + with op.batch_alter_table('_bc_samples', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='unique') + + # ### end Alembic commands ### diff --git a/src/submissions/backend/db/functions.py b/src/submissions/backend/db/functions.py index 2e9ca01..6696247 100644 --- a/src/submissions/backend/db/functions.py +++ b/src/submissions/backend/db/functions.py @@ -18,7 +18,7 @@ from getpass import getuser import numpy as np import yaml from pathlib import Path -from tools import Settings +from tools import Settings, check_regex_match, RSLNamer @@ -43,7 +43,6 @@ def store_submission(ctx:Settings, base_submission:models.BasicSubmission) -> No Returns: None|dict : object that indicates issue raised for reporting in gui """ - from tools import RSLNamer logger.debug(f"Hello from store_submission") # Add all samples to sample table typer = RSLNamer(ctx=ctx, instr=base_submission.rsl_plate_num) @@ -52,7 +51,7 @@ def store_submission(ctx:Settings, base_submission:models.BasicSubmission) -> No logger.debug(f"Typer: {typer.submission_type}") # Suuuuuper hacky way to be sure that the artic doesn't overwrite the ww plate in a ww sample # need something more elegant - if "_artic" not in typer.submission_type: + if "_artic" not in typer.submission_type.lower(): sample.rsl_plate = base_submission else: sample.artic_rsl_plate = base_submission @@ -114,7 +113,7 @@ def construct_submission_info(ctx:Settings, info_dict:dict) -> models.BasicSubmi Returns: models.BasicSubmission: Constructed submission object """ - from tools import check_regex_match, RSLNamer + # from tools import check_regex_match, RSLNamer # convert submission type into model name query = info_dict['submission_type'].replace(" ", "") # Ensure an rsl plate number exists for the plate @@ -127,7 +126,8 @@ def construct_submission_info(ctx:Settings, info_dict:dict) -> models.BasicSubmi info_dict['rsl_plate_num'] = RSLNamer(ctx=ctx, instr=info_dict["rsl_plate_num"]).parsed_name # check database for existing object # instance = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.rsl_plate_num==info_dict['rsl_plate_num']).first() - instance = ctx.database_session.query(models.BasicSubmission).filter(models.BasicSubmission.rsl_plate_num==info_dict['rsl_plate_num']).first() + # instance = ctx.database_session.query(models.BasicSubmission).filter(models.BasicSubmission.rsl_plate_num==info_dict['rsl_plate_num']).first() + instance = lookup_submission_by_rsl_num(ctx=ctx, rsl_num=info_dict['rsl_plate_num']) # get model based on submission type converted above logger.debug(f"Looking at models for submission type: {query}") model = getattr(models, query) @@ -866,8 +866,6 @@ def hitpick_plate(submission:models.BasicSubmission, plate_number:int=0) -> list plate_dicto = [] for sample in submission.samples: # have sample report back its info if it's positive, otherwise, None - method_list = [func for func in dir(sample) if callable(getattr(sample, func))] - logger.debug(f"Method list of sample: {method_list}") samp = sample.to_hitpick() if samp == None: continue @@ -963,7 +961,6 @@ def lookup_last_used_reagenttype_lot(ctx:Settings, type_name:str) -> models.Reag except AttributeError: return None - def check_kit_integrity(sub:BasicSubmission|KitType, reagenttypes:list|None=None) -> dict|None: """ Ensures all reagents expected in kit are listed in Submission diff --git a/src/submissions/backend/db/models/samples.py b/src/submissions/backend/db/models/samples.py index 5b8a2a0..c15abe6 100644 --- a/src/submissions/backend/db/models/samples.py +++ b/src/submissions/backend/db/models/samples.py @@ -18,7 +18,7 @@ class WWSample(Base): id = Column(INTEGER, primary_key=True) #: primary key ww_processing_num = Column(String(64)) #: wastewater processing number - ww_sample_full_id = Column(String(64), nullable=False) + ww_sample_full_id = Column(String(64), nullable=False, unique=True) rsl_number = Column(String(64)) #: rsl plate identification number rsl_plate = relationship("Wastewater", back_populates="samples") #: relationship to parent plate rsl_plate_id = Column(INTEGER, ForeignKey("_submissions.id", ondelete="SET NULL", name="fk_WWS_submission_id")) @@ -111,7 +111,7 @@ class BCSample(Base): id = Column(INTEGER, primary_key=True) #: primary key well_number = Column(String(8)) #: location on parent plate - sample_id = Column(String(64), nullable=False) #: identification from submitter + sample_id = Column(String(64), nullable=False, unique=True) #: identification from submitter organism = Column(String(64)) #: bacterial specimen concentration = Column(String(16)) #: rsl_plate_id = Column(INTEGER, ForeignKey("_submissions.id", ondelete="SET NULL", name="fk_BCS_sample_id")) #: id of parent plate diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index c5e4572..d62681b 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -314,23 +314,29 @@ class SheetParser(object): continue logger.debug(f"massaged sample list for {self.sub['rsl_plate_num']}: {pprint.pprint(return_list)}") return return_list - submission_info = self.xl.parse("cDNA", dtype=object) - biomek_info = self.xl.parse("ArticV4_1 Biomek", dtype=object) - # Reminder that the iloc uses row, column ordering - # sub_reagent_range = submission_info.iloc[56:, 1:4].dropna(how='all') - sub_reagent_range = submission_info.iloc[7:15, 5:9].dropna(how='all') - biomek_reagent_range = biomek_info.iloc[62:, 0:3].dropna(how='all') + submission_info = self.xl.parse("First Strand", dtype=object) + biomek_info = self.xl.parse("ArticV4 Biomek", dtype=object) + sub_reagent_range = submission_info.iloc[56:, 1:4].dropna(how='all') + biomek_reagent_range = biomek_info.iloc[60:, 0:3].dropna(how='all') + # submission_info = self.xl.parse("cDNA", dtype=object) + # biomek_info = self.xl.parse("ArticV4_1 Biomek", dtype=object) + # # Reminder that the iloc uses row, column ordering + # # sub_reagent_range = submission_info.iloc[56:, 1:4].dropna(how='all') + # sub_reagent_range = submission_info.iloc[7:15, 5:9].dropna(how='all') + # biomek_reagent_range = biomek_info.iloc[62:, 0:3].dropna(how='all') self.sub['submitter_plate_num'] = "" self.sub['rsl_plate_num'] = RSLNamer(ctx=self.ctx, instr=self.filepath.__str__()).parsed_name self.sub['submitted_date'] = biomek_info.iloc[1][1] self.sub['submitting_lab'] = "Enterics Wastewater Genomics" - self.sub['sample_count'] = submission_info.iloc[34][6] + self.sub['sample_count'] = submission_info.iloc[4][6] + # self.sub['sample_count'] = submission_info.iloc[34][6] self.sub['extraction_kit'] = "ArticV4.1" self.sub['technician'] = f"MM: {biomek_info.iloc[2][1]}, Bio: {biomek_info.iloc[3][1]}" self.sub['reagents'] = [] parse_reagents(sub_reagent_range) parse_reagents(biomek_reagent_range) - samples = massage_samples(biomek_info.iloc[25:33, 0:]) + samples = massage_samples(biomek_info.iloc[22:31, 0:]) + # samples = massage_samples(biomek_info.iloc[25:33, 0:]) sample_parser = SampleParser(self.ctx, pd.DataFrame.from_records(samples)) sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type']['value'].lower()}_samples") self.sample_result, self.sub['samples'] = sample_parse() diff --git a/src/submissions/backend/pydant/__init__.py b/src/submissions/backend/pydant/__init__.py index 6ec986b..a784f78 100644 --- a/src/submissions/backend/pydant/__init__.py +++ b/src/submissions/backend/pydant/__init__.py @@ -93,7 +93,6 @@ class PydSubmission(BaseModel, extra=Extra.allow): else: return value else: - # logger.debug(f"Pydant values:{type(values)}\n{values}") return dict(value=RSLNamer(ctx=values.data['ctx'], instr=values.data['filepath'].__str__()).parsed_name, parsed=False) @field_validator("technician", mode="before") @@ -115,11 +114,6 @@ class PydSubmission(BaseModel, extra=Extra.allow): return_val = [] for reagent in value: logger.debug(f"Pydantic reagent: {reagent}") - # match reagent.type.lower(): - # case 'atcc': - # continue - # case _: - # return_val.append(reagent) if reagent.type == None: continue else: @@ -132,7 +126,6 @@ class PydSubmission(BaseModel, extra=Extra.allow): if check_not_nan(value): return int(value) else: - # raise ValueError(f"{value} could not be used to create an integer.") return convert_nans_to_nones(value) @field_validator("extraction_kit", mode='before') @@ -142,13 +135,11 @@ class PydSubmission(BaseModel, extra=Extra.allow): if check_not_nan(value): return dict(value=value, parsed=True) else: - # logger.debug(values.data) dlg = KitSelector(ctx=values.data['ctx'], title="Kit Needed", message="At minimum a kit is needed. Please select one.") if dlg.exec(): return dict(value=dlg.getValues(), parsed=False) else: raise ValueError("Extraction kit needed.") - @field_validator("submission_type", mode='before') @classmethod @@ -161,14 +152,3 @@ class PydSubmission(BaseModel, extra=Extra.allow): return dict(value=value.title(), parsed=False) else: return dict(value=RSLNamer(ctx=values.data['ctx'], instr=values.data['filepath'].__str__()).submission_type.title(), parsed=False) - - # @model_validator(mode="after") - # def ensure_kit(cls, values): - # logger.debug(f"Model values: {values}") - # missing_fields = [k for k,v in values if v == None] - # if len(missing_fields) > 0: - # logger.debug(f"Missing fields: {missing_fields}") - # values['missing_fields'] = missing_fields - # return values - - diff --git a/src/submissions/frontend/__init__.py b/src/submissions/frontend/__init__.py index e42372b..2a68924 100644 --- a/src/submissions/frontend/__init__.py +++ b/src/submissions/frontend/__init__.py @@ -1,5 +1,5 @@ ''' -Operations for all user interactions. +Constructs main application. ''' import sys from PyQt6.QtWidgets import ( @@ -31,7 +31,6 @@ class App(QMainWindow): self.ctx = ctx # indicate version and connected database in title bar try: - # self.title = f"Submissions App (v{ctx['package'].__version__}) - {ctx['database']}" self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_path}" except (AttributeError, KeyError): self.title = f"Submissions App" diff --git a/src/submissions/frontend/all_window_functions.py b/src/submissions/frontend/all_window_functions.py index 17fc1aa..4afe9fc 100644 --- a/src/submissions/frontend/all_window_functions.py +++ b/src/submissions/frontend/all_window_functions.py @@ -23,7 +23,10 @@ def select_open_file(obj:QMainWindow, file_extension:str) -> Path: Path: Path of file to be opened """ # home_dir = str(Path(obj.ctx["directory_path"])) - home_dir = str(Path(obj.ctx.directory_path)) + try: + home_dir = Path(obj.ctx.directory_path).resolve().__str__() + except FileNotFoundError: + home_dir = Path.home().resolve().__str__() fname = Path(QFileDialog.getOpenFileName(obj, 'Open file', home_dir, filter = f"{file_extension}(*.{file_extension})")[0]) return fname @@ -43,7 +46,7 @@ def select_save_file(obj:QMainWindow, default_name:str, extension:str) -> Path: # home_dir = Path(obj.ctx["directory_path"]).joinpath(default_name).resolve().__str__() home_dir = Path(obj.ctx.directory_path).joinpath(default_name).resolve().__str__() except FileNotFoundError: - home_dir = Path.home().resolve().__str__() + home_dir = Path.home().joinpath(default_name).resolve().__str__() fname = Path(QFileDialog.getSaveFileName(obj, "Save File", home_dir, filter = f"{extension}(*.{extension})")[0]) return fname diff --git a/src/submissions/frontend/custom_widgets/misc.py b/src/submissions/frontend/custom_widgets/misc.py index ca5cf8f..3c984aa 100644 --- a/src/submissions/frontend/custom_widgets/misc.py +++ b/src/submissions/frontend/custom_widgets/misc.py @@ -230,10 +230,9 @@ class ControlsDatePicker(QWidget): super().__init__() self.start_date = QDateEdit(calendarPopup=True) - # start date is three month prior to end date by default - # NOTE: 2 month, but the variable name is the same cause I'm lazy - threemonthsago = QDate.currentDate().addDays(-60) - self.start_date.setDate(threemonthsago) + # start date is two months prior to end date by default + twomonthsago = QDate.currentDate().addDays(-60) + self.start_date.setDate(twomonthsago) self.end_date = QDateEdit(calendarPopup=True) self.end_date.setDate(QDate.currentDate()) self.layout = QHBoxLayout() @@ -299,4 +298,3 @@ class ImportReagent(QComboBox): logger.debug(f"New relevant reagents: {relevant_reagents}") self.setObjectName(f"lot_{reagent.type}") self.addItems(relevant_reagents) - diff --git a/src/submissions/frontend/custom_widgets/sub_details.py b/src/submissions/frontend/custom_widgets/sub_details.py index a56e107..f9af17e 100644 --- a/src/submissions/frontend/custom_widgets/sub_details.py +++ b/src/submissions/frontend/custom_widgets/sub_details.py @@ -338,7 +338,8 @@ class SubmissionDetails(QDialog): # with open("test.html", "w") as f: # f.write(html) try: - home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__() + # home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__() + home_dir = Path(self.ctx.directory_path).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__() except FileNotFoundError: home_dir = Path.home().resolve().__str__() fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0]) diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py index 666c071..b9c270b 100644 --- a/src/submissions/tools/__init__.py +++ b/src/submissions/tools/__init__.py @@ -136,7 +136,8 @@ def check_if_app(ctx:dict=None) -> bool: def retrieve_rsl_number(in_str:str) -> Tuple[str, str]: """ Uses regex to retrieve the plate number and submission type from an input string - + DEPRECIATED. REPLACED BY RSLNamer.parsed_name + Args: in_str (str): string to be parsed @@ -354,7 +355,7 @@ class Settings(BaseSettings): super_users: list power_users: list rerun_regex: str - submission_types: dict + submission_types: dict|None = None database_session: Session|None = None package: Any|None = None @@ -609,4 +610,4 @@ def jinja_template_loading(): # jinja template loading loader = FileSystemLoader(loader_path) env = Environment(loader=loader) - return env \ No newline at end of file + return env