initial commit

This commit is contained in:
Landon Wark
2023-01-18 14:00:25 -06:00
commit e763e7273d
26 changed files with 2118 additions and 0 deletions

View File

View File

@@ -0,0 +1,4 @@
# __init__.py
# Version of the realpython-reader package
__version__ = "1.0.0"

View File

@@ -0,0 +1,97 @@
import sys
from pathlib import Path
from configure import get_config, create_database_session, setup_logger
ctx = get_config(None)
from PyQt6.QtWidgets import QApplication
from frontend import App
logger = setup_logger(verbose=True)
ctx["database_session"] = create_database_session(Path(ctx['database']))
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App(ctx=ctx)
sys.exit(app.exec())
# from pathlib import Path
# from tkinter import *
# from tkinter import filedialog as fd
# from tkinter import ttk
# from tkinterhtml import HtmlFrame
# from xl_parser import SheetParser
# class Window(Frame):
# def __init__(self, master=None):
# Frame.__init__(self, master)
# self.master = master
# # Frame.pack_propagate(False)
# menu = Menu(self.master)
# self.master.config(menu=menu)
# fileMenu = Menu(menu)
# fileMenu.add_command(label="Import", command=self.import_callback)
# fileMenu.add_command(label="Exit", command=self.exitProgram)
# menu.add_cascade(label="File", menu=fileMenu)
# editMenu = Menu(menu)
# editMenu.add_command(label="Undo")
# editMenu.add_command(label="Redo")
# menu.add_cascade(label="Edit", menu=editMenu)
# tab_parent = ttk.Notebook(self.master)
# self.add_sample_tab = ttk.Frame(tab_parent)
# self.control_view_tab = HtmlFrame(tab_parent)
# tab_parent.add(self.add_sample_tab, text="Add Sample")
# tab_parent.add(self.control_view_tab, text="Controls View")
# tab_parent.pack()
# with open("L:\Robotics Laboratory Support\Quality\Robotics Support Laboratory Extraction Controls\MCS-SSTI.html", "r") as f:
# data = f.read()
# # frame =
# # frame.set_content(data)
# # self.control_view_tab.set_content("""
# # <html>
# # <body>
# # <h1>Hello world!</h1>
# # <p>First para</p>
# # <ul>
# # <li>first list item</li>
# # <li>second list item</li>
# # </ul>
# # <img src="http://findicons.com/files/icons/638/magic_people/128/magic_ball.png"/>
# # </body>
# # </html>
# # """)
# def exitProgram(self):
# exit()
# def import_callback(self):
# name= fd.askopenfilename()
# prsr = SheetParser(Path(name), **ctx)
# for item in prsr.sub:
# lbl=Label(self.add_sample_tab, text=item, fg='red', font=("Helvetica", 16))
# lbl.pack()
# txtfld=Entry(self.add_sample_tab, text="Data not set", bd=2)
# txtfld.pack()
# txtfld.delete(0,END)
# txtfld.insert(0,prsr.sub[item])
# root = Tk()
# app = Window(root)
# # for item in test_data:
# # lbl=Label(root, text=item, fg='red', font=("Helvetica", 16))
# # lbl.pack()
# # txtfld=Entry(root, text="", bd=2)
# # txtfld.pack()
# # txtfld.delete(0,END)
# # txtfld.insert(0,test_data[item])
# root.wm_title("Tkinter window")
# root.mainloop()

View File

View File

@@ -0,0 +1,213 @@
from . import models
import pandas as pd
# from sqlite3 import IntegrityError
from sqlalchemy.exc import IntegrityError
import logging
import datetime
from sqlalchemy import and_
import uuid
import base64
logger = logging.getLogger(__name__)
def get_kits_by_use( ctx:dict, kittype_str:str|None) -> list:
pass
# ctx dict should contain the database session
def store_submission(ctx:dict, base_submission:models.BasicSubmission) -> None:
ctx['database_session'].add(base_submission)
try:
ctx['database_session'].commit()
except IntegrityError:
ctx['database_session'].rollback()
return {"message":"This plate number already exists, so we can't add it."}
return None
def store_reagent(ctx:dict, reagent:models.Reagent) -> None:
print(reagent.__dict__)
ctx['database_session'].add(reagent)
ctx['database_session'].commit()
def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmission:
query = info_dict['submission_type'].replace(" ", "")
model = getattr(models, query)
info_dict['submission_type'] = info_dict['submission_type'].replace(" ", "_").lower()
instance = model()
for item in info_dict:
print(f"Setting {item} to {info_dict[item]}")
match item:
case "extraction_kit":
q_str = info_dict[item]
print(f"Looking up kit {q_str}")
field_value = lookup_kittype_by_name(ctx=ctx, name=q_str)
print(f"Got {field_value} for kit {q_str}")
case "submitting_lab":
q_str = info_dict[item].replace(" ", "_").lower()
print(f"looking up organization: {q_str}")
field_value = lookup_org_by_name(ctx=ctx, name=q_str)
print(f"Got {field_value} for organization {q_str}")
case "submitter_plate_num":
# Because of unique constraint, the submitter plate number cannot be None, so...
if info_dict[item] == None:
info_dict[item] = uuid.uuid4().hex.upper()
case _:
field_value = info_dict[item]
try:
setattr(instance, item, field_value)
except AttributeError:
print(f"Could not set attribute: {item} to {info_dict[item]}")
continue
return instance
# looked_up = []
# for reagent in reagents:
# my_reagent = lookup_reagent(reagent)
# print(my_reagent)
# looked_up.append(my_reagent)
# print(looked_up)
# instance.reagents = looked_up
# ctx['database_session'].add(instance)
# ctx['database_session'].commit()
def construct_reagent(ctx:dict, info_dict:dict) -> models.Reagent:
reagent = models.Reagent()
for item in info_dict:
print(f"Reagent info item: {item}")
match item:
case "lot":
reagent.lot = info_dict[item].upper()
case "expiry":
reagent.expiry = info_dict[item]
case "type":
reagent.type = lookup_reagenttype_by_name(ctx=ctx, rt_name=info_dict[item].replace(" ", "_").lower())
try:
reagent.expiry = reagent.expiry + reagent.type.eol_ext
except TypeError as e:
print(f"WE got a type error: {e}.")
except AttributeError:
pass
return reagent
def lookup_reagent(ctx:dict, reagent_lot:str):
lookedup = ctx['database_session'].query(models.Reagent).filter(models.Reagent.lot==reagent_lot).first()
return lookedup
def get_all_reagenttype_names(ctx:dict) -> list[str]:
lookedup = [item.__str__() for item in ctx['database_session'].query(models.ReagentType).all()]
return lookedup
def lookup_reagenttype_by_name(ctx:dict, rt_name:str) -> models.ReagentType:
print(f"Looking up ReagentType by name: {rt_name}")
lookedup = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name==rt_name).first()
print(f"Found ReagentType: {lookedup}")
return lookedup
def lookup_kittype_by_use(ctx:dict, used_by:str) -> list[models.KitType]:
# return [item for item in
return ctx['database_session'].query(models.KitType).filter(models.KitType.used_for.contains(used_by))
def lookup_kittype_by_name(ctx:dict, name:str) -> models.KitType:
print(f"Querying kittype: {name}")
return ctx['database_session'].query(models.KitType).filter(models.KitType.name==name).first()
def lookup_regent_by_type_name(ctx:dict, type_name:str) -> list[models.ReagentType]:
# return [item for item in ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name==type_name).all()]
return ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name==type_name).all()
def lookup_regent_by_type_name_and_kit_name(ctx:dict, type_name:str, kit_name:str) -> list[models.Reagent]:
# Hang on, this is going to be a long one.
by_type = ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name.endswith(type_name))
add_in = by_type.join(models.ReagentType.kits).filter(models.KitType.name==kit_name)
return add_in
def lookup_all_submissions_by_type(ctx:dict, type:str|None=None):
if type == None:
subs = ctx['database_session'].query(models.BasicSubmission).all()
else:
subs = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.submission_type==type).all()
return subs
def lookup_all_orgs(ctx:dict) -> list[models.Organization]:
return ctx['database_session'].query(models.Organization).all()
def lookup_org_by_name(ctx:dict, name:str|None) -> models.Organization:
print(f"Querying organization: {name}")
return ctx['database_session'].query(models.Organization).filter(models.Organization.name==name).first()
def submissions_to_df(ctx:dict, type:str|None=None):
print(f"Type: {type}")
subs = [item.to_dict() for item in lookup_all_submissions_by_type(ctx=ctx, type=type)]
df = pd.DataFrame.from_records(subs)
return df
def lookup_submission_by_id(ctx:dict, id:int) -> models.BasicSubmission:
return ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.id==id).first()
def create_submission_details(ctx:dict, sub_id:int) -> dict:
pass
def lookup_submissions_by_date_range(ctx:dict, start_date:datetime.date, end_date:datetime.date) -> list[models.BasicSubmission]:
return ctx['database_session'].query(models.BasicSubmission).filter(and_(models.BasicSubmission.submitted_date > start_date, models.BasicSubmission.submitted_date < end_date)).all()
def get_all_Control_Types_names(ctx:dict) -> list[models.ControlType]:
"""
Grabs all control type names from db.
Args:
settings (dict): settings passed down from click. Defaults to {}.
Returns:
list: names list
"""
conTypes = ctx['database_session'].query(models.ControlType).all()
conTypes = [conType.name for conType in conTypes]
logger.debug(f"Control Types: {conTypes}")
return conTypes
def create_kit_from_yaml(ctx:dict, exp:dict) -> None:
"""
Create and store a new kit in the database based on a .yml file
Args:
ctx (dict): Context dictionary passed down from frontend
exp (dict): Experiment dictionary created from yaml file
"""
if base64.b64encode(exp['password']) != b'cnNsX3N1Ym1pNTVpb25z':
print(f"Not the correct password.")
return
for type in exp:
if type == "password":
continue
for kt in exp[type]['kits']:
kit = models.KitType(name=kt, used_for=[type.replace("_", " ").title()], cost_per_run=exp[type]["kits"][kt]["cost"])
for r in exp[type]['kits'][kt]['reagenttypes']:
look_up = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name==r).first()
if look_up == None:
rt = models.ReagentType(name=r.replace(" ", "_").lower(), eol_ext=datetime.timedelta(30*exp[type]['kits'][kt]['reagenttypes'][r]['eol_ext']), kits=[kit])
else:
rt = look_up
rt.kits.append(kit)
ctx['database_session'].add(rt)
print(rt.__dict__)
print(kit.__dict__)
ctx['database_session'].add(kit)
ctx['database_session'].commit()
def lookup_all_sample_types(ctx:dict) -> list[str]:
uses = [item.used_for for item in ctx['database_session'].query(models.KitType).all()]
uses = list(set([item for sublist in uses for item in sublist]))
return uses

View File

@@ -0,0 +1,11 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
metadata = Base.metadata
from .controls import Control, ControlType
from .kits import KitType, ReagentType, Reagent
from .submissions import BasicSubmission, BacterialCulture, Wastewater
from .organizations import Organization, Contact
from .samples import Sample

View File

@@ -0,0 +1,36 @@
from . import Base
from sqlalchemy import Column, String, TIMESTAMP, text, JSON, INTEGER, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship
class ControlType(Base):
"""
Base class of a control archetype.
"""
__tablename__ = '_control_types'
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(255), unique=True) #: controltype name (e.g. MCS)
targets = Column(JSON) #: organisms checked for
# instances_id = Column(INTEGER, ForeignKey("_control_samples.id", ondelete="SET NULL", name="fk_ctype_instances_id"))
instances = relationship("Control", back_populates="controltype") #: control samples created of this type.
# UniqueConstraint('name', name='uq_controltype_name')
class Control(Base):
"""
Base class of a control sample.
"""
__tablename__ = '_control_samples'
id = Column(INTEGER, primary_key=True) #: primary key
parent_id = Column(String, ForeignKey("_control_types.id", name="fk_control_parent_id")) #: primary key of control type
controltype = relationship("ControlType", back_populates="instances", foreign_keys=[parent_id]) #: reference to parent control type
name = Column(String(255), unique=True) #: Sample ID
submitted_date = Column(TIMESTAMP) #: Date submitted to Robotics
contains = Column(JSON) #: unstructured hashes in contains.tsv for each organism
matches = Column(JSON) #: unstructured hashes in matches.tsv for each organism
kraken = Column(JSON) #: unstructured output from kraken_report
# UniqueConstraint('name', name='uq_control_name')
submissions = relationship("BacterialCulture", back_populates="control")

View File

@@ -0,0 +1,68 @@
from . import Base
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT
from sqlalchemy.orm import relationship
reagenttypes_kittypes = Table("_reagentstypes_kittypes", Base.metadata, Column("reagent_types_id", INTEGER, ForeignKey("_reagent_types.id")), Column("kits_id", INTEGER, ForeignKey("_kits.id")))
class KitType(Base):
__tablename__ = "_kits"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True)
submissions = relationship("BasicSubmission", back_populates="extraction_kit")
used_for = Column(JSON)
cost_per_run = Column(FLOAT(2))
reagent_types = relationship("ReagentType", back_populates="kits", uselist=True, secondary=reagenttypes_kittypes)
reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id", ondelete='SET NULL', use_alter=True, name="fk_KT_reagentstype_id"))
def __str__(self):
return self.name
class ReagentType(Base):
__tablename__ = "_reagent_types"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64))
kit_id = Column(INTEGER, ForeignKey("_kits.id", ondelete="SET NULL", use_alter=True, name="fk_RT_kits_id"))
kits = relationship("KitType", back_populates="reagent_types", uselist=True, foreign_keys=[kit_id])
instances = relationship("Reagent", back_populates="type")
# instances_id = Column(INTEGER, ForeignKey("_reagents.id", ondelete='SET NULL'))
eol_ext = Column(Interval())
def __str__(self):
return self.name
class Reagent(Base):
__tablename__ = "_reagents"
id = Column(INTEGER, primary_key=True) #: primary key
type = relationship("ReagentType", back_populates="instances")
type_id = Column(INTEGER, ForeignKey("_reagent_types.id", ondelete='SET NULL', name="fk_reagent_type_id"))
name = Column(String(64))
lot = Column(String(64))
expiry = Column(TIMESTAMP)
submissions = relationship("BasicSubmission", back_populates="reagents", uselist=True)
def __str__(self):
return self.lot
def to_sub_dict(self):
try:
type = self.type.name.replace("_", " ").title()
except AttributeError:
type = "Unknown"
return {
"type": type,
"lot": self.lot,
"expiry": self.expiry.strftime("%Y-%m-%d")
}

View File

@@ -0,0 +1,34 @@
from . import Base
from sqlalchemy import Column, String, TIMESTAMP, JSON, Float, INTEGER, ForeignKey, UniqueConstraint, Table
from sqlalchemy.orm import relationship, validates
orgs_contacts = Table("_orgs_contacts", Base.metadata, Column("org_id", INTEGER, ForeignKey("_organizations.id")), Column("contact_id", INTEGER, ForeignKey("_contacts.id")))
class Organization(Base):
__tablename__ = "_organizations"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64))
submissions = relationship("BasicSubmission", back_populates="submitting_lab")
cost_centre = Column(String())
contacts = relationship("Contact", back_populates="organization", secondary=orgs_contacts)
contact_ids = Column(INTEGER, ForeignKey("_contacts.id", ondelete="SET NULL", name="fk_org_contact_id"))
def __str__(self):
return self.name.replace("_", " ").title()
class Contact(Base):
__tablename__ = "_contacts"
id = id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64))
email = Column(String(64))
phone = Column(String(32))
organization = relationship("Organization", back_populates="contacts", uselist=True)
# organization_id = Column(INTEGER, ForeignKey("_organizations.id"))

View File

@@ -0,0 +1,27 @@
from . import Base
from sqlalchemy import Column, String, TIMESTAMP, text, JSON, INTEGER, ForeignKey, FLOAT, BOOLEAN
from sqlalchemy.orm import relationship, relationships
class Sample(Base):
__tablename__ = "_ww_samples"
id = Column(INTEGER, primary_key=True) #: primary key
ww_processing_num = Column(String(64))
ww_sample_full_id = Column(String(64))
rsl_number = Column(String(64))
rsl_plate = relationship("Wastewater", back_populates="samples")
collection_date = Column(TIMESTAMP) #: Date submission received
testing_type = Column(String(64))
site_status = Column(String(64))
notes = Column(String(2000))
ct_n1 = Column(FLOAT(2))
ct_n2 = Column(FLOAT(2))
seq_submitted = Column(BOOLEAN())
ww_seq_run_id = Column(String(64))
sample_type = Column(String(8))

View File

@@ -0,0 +1,103 @@
from . import Base
from sqlalchemy import Column, String, TIMESTAMP, text, JSON, INTEGER, ForeignKey, UniqueConstraint, Table
from sqlalchemy.orm import relationship, relationships
from datetime import datetime as dt
reagents_submissions = Table("_reagents_submissions", Base.metadata, Column("reagent_id", INTEGER, ForeignKey("_reagents.id")), Column("submission_id", INTEGER, ForeignKey("_submissions.id")))
class BasicSubmission(Base):
# TODO: Figure out if I want seperate tables for different sample types.
__tablename__ = "_submissions"
id = Column(INTEGER, primary_key=True) #: primary key
rsl_plate_num = Column(String(32), unique=True) #: RSL name (e.g. RSL-22-0012)
submitter_plate_num = Column(String(127), unique=True) #: The number given to the submission by the submitting lab
submitted_date = Column(TIMESTAMP) #: Date submission received
submitting_lab = relationship("Organization", back_populates="submissions") #: client
submitting_lab_id = Column(INTEGER, ForeignKey("_organizations.id", ondelete="SET NULL"))
sample_count = Column(INTEGER) #: Number of samples in the submission
extraction_kit = relationship("KitType", back_populates="submissions") #: The extraction kit used
extraction_kit_id = Column(INTEGER, ForeignKey("_kits.id", ondelete="SET NULL"))
submission_type = Column(String(32))
technician = Column(String(64))
# Move this into custom types?
reagents = relationship("Reagent", back_populates="submissions", secondary=reagents_submissions)
reagents_id = Column(String, ForeignKey("_reagents.id", ondelete="SET NULL", name="fk_BS_reagents_id"))
__mapper_args__ = {
"polymorphic_identity": "basic_submission",
"polymorphic_on": submission_type,
"with_polymorphic": "*",
}
def to_dict(self):
print(self.submitting_lab)
try:
sub_lab = self.submitting_lab.name
except AttributeError:
sub_lab = None
try:
sub_lab = sub_lab.replace("_", " ").title()
except AttributeError:
pass
try:
ext_kit = self.extraction_kit.name
except AttributeError:
ext_kit = None
output = {
"id": self.id,
"Plate Number": self.rsl_plate_num,
"Submission Type": self.submission_type.replace("_", " ").title(),
"Submitter Plate Number": self.submitter_plate_num,
"Submitted Date": self.submitted_date.strftime("%Y-%m-%d"),
"Submitting Lab": sub_lab,
"Sample Count": self.sample_count,
"Extraction Kit": ext_kit,
"Technician": self.technician,
}
return output
def report_dict(self):
try:
sub_lab = self.submitting_lab.name
except AttributeError:
sub_lab = None
try:
sub_lab = sub_lab.replace("_", " ").title()
except AttributeError:
pass
try:
ext_kit = self.extraction_kit.name
except AttributeError:
ext_kit = None
try:
cost = self.extraction_kit.cost_per_run
except AttributeError:
cost = None
output = {
"id": self.id,
"Plate Number": self.rsl_plate_num,
"Submission Type": self.submission_type.replace("_", " ").title(),
"Submitter Plate Number": self.submitter_plate_num,
"Submitted Date": self.submitted_date.strftime("%Y-%m-%d"),
"Submitting Lab": sub_lab,
"Sample Count": self.sample_count,
"Extraction Kit": ext_kit,
"Cost": cost
}
return output
# Below are the custom submission
class BacterialCulture(BasicSubmission):
control = relationship("Control", back_populates="submissions") #: A control sample added to submission
control_id = Column(INTEGER, ForeignKey("_control_samples.id", ondelete="SET NULL", name="fk_BC_control_id"))
__mapper_args__ = {"polymorphic_identity": "bacterial_culture", "polymorphic_load": "inline"}
class Wastewater(BasicSubmission):
samples = relationship("Sample", back_populates="rsl_plate")
sample_id = Column(String, ForeignKey("_ww_samples.id", ondelete="SET NULL", name="fk_WW_sample_id"))
__mapper_args__ = {"polymorphic_identity": "wastewater", "polymorphic_load": "inline"}

View File

@@ -0,0 +1,122 @@
import pandas as pd
from pathlib import Path
from datetime import datetime
import logging
from collections import OrderedDict
import re
logger = logging.getLogger(f"submissions.{__name__}")
class SheetParser(object):
def __init__(self, filepath:Path|None = None, **kwargs):
for kwarg in kwargs:
setattr(self, f"_{kwarg}", kwargs[kwarg])
if filepath == None:
self.xl = None
else:
try:
self.xl = pd.ExcelFile(filepath.__str__())
except ValueError:
self.xl = None
self.sub = OrderedDict()
self.sub['submission_type'] = self._type_decider()
parse = getattr(self, f"_parse_{self.sub['submission_type'].lower()}")
parse()
def _type_decider(self):
try:
for type in self._submission_types:
if self.xl.sheet_names == self._submission_types[type]['excel_map']:
return type.title()
return "Unknown"
except:
return "Unknown"
def _parse_unknown(self):
self.sub = None
def _parse_generic(self, sheet_name:str):
submission_info = self.xl.parse(sheet_name=sheet_name)
self.sub['submitter_plate_num'] = submission_info.iloc[0][1]
self.sub['rsl_plate_num'] = str(submission_info.iloc[10][1])
self.sub['submitted_date'] = submission_info.iloc[1][1].date()#.strftime("%Y-%m-%d")
self.sub['submitting_lab'] = submission_info.iloc[0][3]
self.sub['sample_count'] = str(submission_info.iloc[2][3])
self.sub['extraction_kit'] = submission_info.iloc[3][3]
return submission_info
def _parse_bacterial_culture(self):
# submission_info = self.xl.parse("Sample List")
submission_info = self._parse_generic("Sample List")
# iloc is [row][column] and the first row is set as header row so -2
tech = str(submission_info.iloc[11][1])
if tech == "nan":
tech = "Unknown"
elif len(tech.split(",")) > 1:
tech_reg = re.compile(r"[A-Z]{2}")
tech = ", ".join(tech_reg.findall(tech))
self.sub['technician'] = tech
# reagents
self.sub['lot_wash_1'] = submission_info.iloc[1][6]
self.sub['lot_wash_2'] = submission_info.iloc[2][6]
self.sub['lot_binding_buffer'] = submission_info.iloc[3][6]
self.sub['lot_magnetic_beads'] = submission_info.iloc[4][6]
self.sub['lot_lysis_buffer'] = submission_info.iloc[5][6]
self.sub['lot_elution_buffer'] = submission_info.iloc[6][6]
self.sub['lot_isopropanol'] = submission_info.iloc[9][6]
self.sub['lot_ethanol'] = submission_info.iloc[10][6]
self.sub['lot_positive_control'] = submission_info.iloc[103][1]
self.sub['lot_plate'] = submission_info.iloc[12][6]
def _parse_wastewater(self):
# submission_info = self.xl.parse("WW Submissions (ENTER HERE)")
submission_info = self._parse_generic("WW Submissions (ENTER HERE)")
enrichment_info = self.xl.parse("Enrichment Worksheet")
extraction_info = self.xl.parse("Extraction Worksheet")
qprc_info = self.xl.parse("qPCR Worksheet")
# iloc is [row][column] and the first row is set as header row so -2
# self.sub['submitter_plate_num'] = submission_info.iloc[0][1]
# self.sub['rsl_plate_num'] = str(submission_info.iloc[10][1])
# self.sub['submitted_date'] = submission_info.iloc[1][1].date()#.strftime("%Y-%m-%d")
# self.sub['submitting_lab'] = submission_info.iloc[0][3]
# self.sub['sample_count'] = str(submission_info.iloc[2][3])
# self.sub['extraction_kit'] = submission_info.iloc[3][3]
self.sub['technician'] = f"Enr: {enrichment_info.columns[2]}, Ext: {extraction_info.columns[2]}, PCR: {qprc_info.columns[2]}"
# reagents
self.sub['lot_lysis_buffer'] = enrichment_info.iloc[0][14]
self.sub['lot_proteinase_K'] = enrichment_info.iloc[1][14]
self.sub['lot_magnetic_virus_particles'] = enrichment_info.iloc[2][14]
self.sub['lot_enrichment_reagent_1'] = enrichment_info.iloc[3][14]
self.sub['lot_binding_buffer'] = extraction_info.iloc[0][14]
self.sub['lot_magnetic_beads'] = extraction_info.iloc[1][14]
self.sub['lot_wash'] = extraction_info.iloc[2][14]
self.sub['lot_ethanol'] = extraction_info.iloc[3][14]
self.sub['lot_elution_buffer'] = extraction_info.iloc[4][14]
self.sub['lot_master_mix'] = qprc_info.iloc[0][14]
self.sub['lot_pre_mix_1'] = qprc_info.iloc[1][14]
self.sub['lot_pre_mix_2'] = qprc_info.iloc[2][14]
self.sub['lot_positive_control'] = qprc_info.iloc[3][14]
self.sub['lot_ddh2o'] = qprc_info.iloc[4][14]
# tech = str(submission_info.iloc[11][1])
# if tech == "nan":
# tech = "Unknown"
# elif len(tech.split(",")) > 1:
# tech_reg = re.compile(r"[A-Z]{2}")
# tech = ", ".join(tech_reg.findall(tech))
# self.sub['lot_wash_1'] = submission_info.iloc[1][6]
# self.sub['lot_wash_2'] = submission_info.iloc[2][6]
# self.sub['lot_binding_buffer'] = submission_info.iloc[3][6]
# self.sub['lot_magnetic_beads'] = submission_info.iloc[4][6]
# self.sub['lot_lysis_buffer'] = submission_info.iloc[5][6]
# self.sub['lot_elution_buffer'] = submission_info.iloc[6][6]
# self.sub['lot_isopropanol'] = submission_info.iloc[9][6]
# self.sub['lot_ethanol'] = submission_info.iloc[10][6]
# self.sub['lot_positive_control'] = None #submission_info.iloc[103][1]
# self.sub['lot_plate'] = submission_info.iloc[12][6]

View File

@@ -0,0 +1,13 @@
from pandas import DataFrame
import numpy as np
def make_report_xlsx(records:list[dict]) -> DataFrame:
df = DataFrame.from_records(records)
df = df.sort_values("Submitting Lab")
# table = df.pivot_table(values="Cost", index=["Submitting Lab", "Extraction Kit"], columns=["Cost", "Sample Count"], aggfunc={'Cost':np.sum,'Sample Count':np.sum})
df2 = df.groupby(["Submitting Lab", "Extraction Kit"]).agg({'Cost': ['sum', 'count'], 'Sample Count':['sum']})
# df2['Cost'] = df2['Cost'].map('${:,.2f}'.format)
print(df2.columns)
# df2['Cost']['sum'] = df2['Cost']['sum'].apply('${:,.2f}'.format)
df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')] = df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')].applymap('${:,.2f}'.format)
return df2

View File

@@ -0,0 +1,194 @@
import yaml
import sys, os, stat, platform
import logging
from logging import handlers
from pathlib import Path
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
logger = logging.getLogger(__name__)
package_dir = Path(__file__).parents[2].resolve()
logger.debug(f"Package dir: {package_dir}")
if platform.system == "Windows":
os_config_dir = "AppData"
logger.debug(f"Got platform Windows, config_dir: {os_config_dir}")
else:
os_config_dir = ".config"
logger.debug(f"Got platform other, config_dir: {os_config_dir}")
main_aux_dir = Path.home().joinpath(f"{os_config_dir}/submissions")
CONFIGDIR = main_aux_dir.joinpath("config")
LOGDIR = main_aux_dir.joinpath("logs")
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
def doRollover(self):
"""
Override base class method to make the new log file group writable.
"""
# Rotate the file first.
handlers.RotatingFileHandler.doRollover(self)
# Add group write to the current permissions.
currMode = os.stat(self.baseFilename).st_mode
os.chmod(self.baseFilename, currMode | stat.S_IWGRP)
def _open(self):
prevumask=os.umask(0o002)
#os.fdopen(os.open('/path/to/file', os.O_WRONLY, 0600))
rtv=handlers.RotatingFileHandler._open(self)
os.umask(prevumask)
return rtv
class StreamToLogger(object):
"""
Fake file-like stream object that redirects writes to a logger instance.
"""
def __init__(self, logger, log_level=logging.INFO):
self.logger = logger
self.log_level = log_level
self.linebuf = ''
def write(self, buf):
for line in buf.rstrip().splitlines():
self.logger.log(self.log_level, line.rstrip())
def get_config(settings_path: str|None) -> dict:
"""Get configuration settings from path or default if blank.
Args:
settings_path (str, optional): _description_. Defaults to "".
Returns:
setting: dictionary of settings.
"""
# with open("C:\\Users\\lwark\\Desktop\\packagedir.txt", "w") as f:
# f.write(package_dir.__str__())
def join(loader, node):
seq = loader.construct_sequence(node)
return ''.join([str(i) for i in seq])
## register the tag handler
yaml.add_constructor('!join', join)
# if user hasn't defined config path in cli args
if settings_path == None:
# Check user .config/ozma directory
# if Path.exists(Path.joinpath(CONFIGDIR, "config.yml")):
# settings_path = Path.joinpath(CONFIGDIR, "config.yml")
if CONFIGDIR.joinpath("config.yml").exists():
settings_path = CONFIGDIR.joinpath("config.yml")
# Check user .ozma directory
elif Path.home().joinpath(".submissions", "config.yml").exists():
settings_path = Path.home().joinpath(".submissions", "config.yml")
# finally look in the local config
else:
if getattr(sys, 'frozen', False):
settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml")
else:
settings_path = package_dir.joinpath('config.yml')
else:
if Path(settings_path).is_dir():
settings_path = settings_path.joinpath("config.yml")
elif Path(settings_path).is_file():
settings_path = settings_path
else:
logger.error("No config.yml file found. Using empty dictionary.")
return {}
logger.debug(f"Using {settings_path} for config file.")
with open(settings_path, "r") as stream:
try:
settings = yaml.load(stream, Loader=yaml.Loader)
except yaml.YAMLError as exc:
logger.error(f'Error reading yaml file {settings_path}: {exc}')
return {}
return settings
def create_database_session(database_path: Path|None) -> Session:
"""Get database settings from path or default if blank.
Args:
database_path (str, optional): _description_. Defaults to "".
Returns:
database_path: string of database path
"""
if database_path == None:
if Path.home().joinpath(".submissions", "submissions.db").exists():
database_path = Path.home().joinpath(".submissions", "submissions.db")
# finally, look in the local dir
else:
database_path = package_dir.joinpath("submissions.db")
else:
if database_path.is_dir():
database_path = database_path.joinpath("submissions.db")
elif database_path.is_file():
database_path = database_path
else:
logger.error("No database file found. Exiting program.")
sys.exit()
logger.debug(f"Using {database_path} for database file.")
engine = create_engine(f"sqlite:///{database_path}")
session = Session(engine)
return session
def setup_logger(verbose:bool=False):
"""Set logger levels using settings.
Args:
verbose (bool, optional): _description_. Defaults to False.
Returns:
logger: logger object
"""
logger = logging.getLogger("submissions")
logger.setLevel(logging.DEBUG)
# create file handler which logs even debug messages
try:
fh = GroupWriteRotatingFileHandler(LOGDIR.joinpath('submissions.log'), mode='a', maxBytes=100000, backupCount=3, encoding=None, delay=False)
except FileNotFoundError as e:
Path(LOGDIR).mkdir(parents=True, exist_ok=True)
fh = GroupWriteRotatingFileHandler(LOGDIR.joinpath('submissions.log'), mode='a', maxBytes=100000, backupCount=3, encoding=None, delay=False)
fh.setLevel(logging.DEBUG)
fh.name = "File"
# create console handler with a higher log level
ch = logging.StreamHandler()
if verbose:
ch.setLevel(logging.DEBUG)
else:
ch.setLevel(logging.WARNING)
ch.name = "Stream"
# create formatter and add it to the handlers
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
ch.setLevel(logging.ERROR)
# add the handlers to the logger
logger.addHandler(fh)
logger.addHandler(ch)
stderr_logger = logging.getLogger('STDERR')
return logger
# sl = StreamToLogger(stderr_logger, logging.ERROR)
# sys.stderr = sl
def set_logger_verbosity(verbosity):
"""Does what it says.
"""
handler = [item for item in logger.parent.handlers if item.name == "Stream"][0]
match verbosity:
case 3:
handler.setLevel(logging.DEBUG)
case 2:
handler.setLevel(logging.INFO)
case 1:
handler.setLevel(logging.WARNING)

View File

@@ -0,0 +1,471 @@
import re
from PyQt6.QtWidgets import (
QMainWindow, QLabel, QToolBar, QStatusBar,
QTabWidget, QWidget, QVBoxLayout,
QPushButton, QMenuBar, QFileDialog,
QLineEdit, QMessageBox, QComboBox, QDateEdit, QHBoxLayout,
QSpinBox
)
from PyQt6.QtGui import QAction, QIcon
from PyQt6.QtCore import QDateTime, QDate
from PyQt6.QtCore import pyqtSlot
from PyQt6.QtWebEngineWidgets import QWebEngineView
import pandas as pd
from pathlib import Path
import plotly
import plotly.express as px
import yaml
from backend.excel.parser import SheetParser
from backend.db import (construct_submission_info, lookup_reagent,
construct_reagent, store_reagent, store_submission, lookup_kittype_by_use,
lookup_regent_by_type_name_and_kit_name, lookup_all_orgs, lookup_submissions_by_date_range,
get_all_Control_Types_names, create_kit_from_yaml
)
from backend.excel.reports import make_report_xlsx
import numpy
from frontend.custom_widgets import AddReagentQuestion, AddReagentForm, SubmissionsSheet, ReportDatePicker, KitAdder
import logging
import difflib
logger = logging.getLogger(__name__)
logger.info("Hello, I am a logger")
class App(QMainWindow):
# class App(QScrollArea):
def __init__(self, ctx: dict = {}):
super().__init__()
self.ctx = ctx
self.title = 'Submissions App - PyQT6'
self.left = 0
self.top = 0
self.width = 1300
self.height = 1000
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.table_widget = AddSubForm(self)
self.setCentralWidget(self.table_widget)
self._createActions()
self._createMenuBar()
self._createToolBar()
self._connectActions()
self.renderPage()
self.show()
def _createMenuBar(self):
menuBar = self.menuBar()
fileMenu = menuBar.addMenu("&File")
# menuBar.addMenu(fileMenu)
# Creating menus using a title
editMenu = menuBar.addMenu("&Edit")
reportMenu = menuBar.addMenu("&Reports")
helpMenu = menuBar.addMenu("&Help")
fileMenu.addAction(self.importAction)
reportMenu.addAction(self.generateReportAction)
def _createToolBar(self):
toolbar = QToolBar("My main toolbar")
self.addToolBar(toolbar)
toolbar.addAction(self.addReagentAction)
toolbar.addAction(self.addKitAction)
def _createActions(self):
self.importAction = QAction("&Import", self)
self.addReagentAction = QAction("Add Reagent", self)
self.generateReportAction = QAction("Make Report", self)
self.addKitAction = QAction("Add Kit", self)
def _connectActions(self):
self.importAction.triggered.connect(self.importSubmission)
self.addReagentAction.triggered.connect(self.add_reagent)
self.generateReportAction.triggered.connect(self.generateReport)
self.addKitAction.triggered.connect(self.add_kit)
def importSubmission(self):
logger.debug(self.ctx)
home_dir = str(Path(self.ctx["directory_path"]))
fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir)[0])
logger.debug(f"Attempting to parse file: {fname}")
assert fname.exists()
try:
prsr = SheetParser(fname, **self.ctx)
except PermissionError:
return
print(f"prsr.sub = {prsr.sub}")
# replace formlayout with tab1.layout
for item in self.table_widget.formlayout.parentWidget().findChildren(QWidget):
item.setParent(None)
variable_parser = re.compile(r"""
# (?x)
(?P<extraction_kit>^extraction_kit$) |
(?P<submitted_date>^submitted_date$) |
(?P<submitting_lab>)^submitting_lab$ |
(?P<reagent>^lot_.*$)
""", re.VERBOSE)
for item in prsr.sub:
logger.debug(f"Item: {item}")
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
try:
mo = variable_parser.fullmatch(item).lastgroup
except AttributeError:
mo = "other"
match mo:
case 'submitting_lab':
print(f"{item}: {prsr.sub[item]}")
add_widget = QComboBox()
labs = [item.__str__() for item in lookup_all_orgs(ctx=self.ctx)]
try:
labs = difflib.get_close_matches(prsr.sub[item], labs, len(labs), 0)
except TypeError:
pass
add_widget.addItems(labs)
case 'extraction_kit':
if prsr.sub[item] == 'nan':
msg = QMessageBox()
# msg.setIcon(QMessageBox.critical)
msg.setText("Error")
msg.setInformativeText('You need to enter a value for extraction kit.')
msg.setWindowTitle("Error")
msg.exec()
break
add_widget = QComboBox()
uses = [item.__str__() for item in lookup_kittype_by_use(ctx=self.ctx, used_by=prsr.sub['submission_type'])]
if len(uses) > 0:
add_widget.addItems(uses)
else:
add_widget.addItems(['bacterial_culture'])
case 'submitted_date':
add_widget = QDateEdit(calendarPopup=True)
# add_widget.setDateTime(QDateTime.date(prsr.sub[item]))
add_widget.setDate(prsr.sub[item])
case 'reagent':
add_widget = QComboBox()
add_widget.setEditable(True)
# Ensure that all reagenttypes have a name that matches the items in the excel parser
query_var = item.replace("lot_", "")
print(f"Query for: {query_var}")
if isinstance(prsr.sub[item], numpy.float64):
print(f"{prsr.sub[item]} is a numpy float!")
try:
prsr.sub[item] = int(prsr.sub[item])
except ValueError:
pass
relevant_reagents = [item.__str__() for item in lookup_regent_by_type_name_and_kit_name(ctx=self.ctx, type_name=query_var, kit_name=prsr.sub['extraction_kit'])]
print(f"Relevant reagents: {relevant_reagents}")
if prsr.sub[item] not in relevant_reagents and prsr.sub[item] != 'nan':
try:
check = not numpy.isnan(prsr.sub[item])
except TypeError:
check = True
if check:
relevant_reagents.insert(0, str(prsr.sub[item]))
logger.debug(f"Relevant reagents: {relevant_reagents}")
add_widget.addItems(relevant_reagents)
case _:
add_widget = QLineEdit()
add_widget.setText(str(prsr.sub[item]).replace("_", " "))
self.table_widget.formlayout.addWidget(add_widget)
submit_btn = QPushButton("Submit")
self.table_widget.formlayout.addWidget(submit_btn)
submit_btn.clicked.connect(self.submit_new_sample)
def renderPage(self):
"""
Test function for plotly chart rendering
"""
df = pd.read_excel("C:\\Users\\lwark\\Desktop\\test_df.xlsx", engine="openpyxl")
fig = px.bar(df, x="submitted_date", y="kraken_percent", color="genus", title="Long-Form Input")
fig.update_layout(
xaxis_title="Submitted Date (* - Date parsed from fastq file creation date)",
yaxis_title="Kraken Percent",
showlegend=True,
barmode='stack'
)
html = '<html><body>'
html += plotly.offline.plot(fig, output_type='div', include_plotlyjs='cdn', auto_open=True, image = 'png', image_filename='plot_image')
html += '</body></html>'
self.table_widget.webengineview.setHtml(html)
self.table_widget.webengineview.update()
def submit_new_sample(self):
labels, values = self.extract_form_info(self.table_widget.tab1)
info = {item[0]:item[1] for item in zip(labels, values) if not item[0].startswith("lot_")}
reagents = {item[0]:item[1] for item in zip(labels, values) if item[0].startswith("lot_")}
logger.debug(f"Reagents: {reagents}")
parsed_reagents = []
for reagent in reagents:
wanted_reagent = lookup_reagent(ctx=self.ctx, reagent_lot=reagents[reagent])
logger.debug(wanted_reagent)
if wanted_reagent == None:
dlg = AddReagentQuestion(reagent_type=reagent, reagent_lot=reagents[reagent])
if dlg.exec():
wanted_reagent = self.add_reagent(reagent_lot=reagents[reagent], reagent_type=reagent.replace("lot_", ""))
else:
logger.debug("Will not add reagent.")
if wanted_reagent != None:
parsed_reagents.append(wanted_reagent)
logger.debug(info)
base_submission = construct_submission_info(ctx=self.ctx, info_dict=info)
for reagent in parsed_reagents:
base_submission.reagents.append(reagent)
result = store_submission(ctx=self.ctx, base_submission=base_submission)
if result != None:
msg = QMessageBox()
# msg.setIcon(QMessageBox.critical)
msg.setText("Error")
msg.setInformativeText(result['message'])
msg.setWindowTitle("Error")
msg.exec()
self.table_widget.sub_wid.setData()
def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None):
if isinstance(reagent_lot, bool):
reagent_lot = ""
dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type)
if dlg.exec():
labels, values = self.extract_form_info(dlg)
info = {item[0]:item[1] for item in zip(labels, values)}
logger.debug(f"Reagent info: {info}")
reagent = construct_reagent(ctx=self.ctx, info_dict=info)
store_reagent(ctx=self.ctx, reagent=reagent)
return reagent
def extract_form_info(self, object):
labels = []
values = []
for item in object.layout.parentWidget().findChildren(QWidget):
match item:
case QLabel():
labels.append(item.text().replace(" ", "_").lower())
case QLineEdit():
# ad hoc check to prevent double reporting of qdatedit under lineedit for some reason
if not isinstance(prev_item, QDateEdit) and not isinstance(prev_item, QComboBox) and not isinstance(prev_item, QSpinBox):
print(f"Previous: {prev_item}")
print(f"Item: {item}")
values.append(item.text())
case QComboBox():
values.append(item.currentText())
case QDateEdit():
values.append(item.date().toPyDate())
prev_item = item
return labels, values
def generateReport(self):
dlg = ReportDatePicker()
if dlg.exec():
labels, values = self.extract_form_info(dlg)
info = {item[0]:item[1] for item in zip(labels, values)}
subs = lookup_submissions_by_date_range(ctx=self.ctx, start_date=info['start_date'], end_date=info['end_date'])
records = [item.report_dict() for item in subs]
df = make_report_xlsx(records=records)
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submissions_{info['start_date']}-{info['end_date']}.xlsx").resolve().__str__()
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".xlsx")[0])
try:
df.to_excel(fname, engine="openpyxl")
except PermissionError:
pass
def add_kit(self):
home_dir = str(Path(self.ctx["directory_path"]))
fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir)[0])
assert fname.exists()
with open(fname.__str__(), "r") as stream:
try:
exp = yaml.load(stream, Loader=yaml.Loader)
except yaml.YAMLError as exc:
logger.error(f'Error reading yaml file {fname}: {exc}')
return {}
create_kit_from_yaml(ctx=self.ctx, exp=exp)
class AddSubForm(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
# Initialize tab screen
self.tabs = QTabWidget()
self.tab1 = QWidget()
self.tab2 = QWidget()
self.tab3 = QWidget()
self.tabs.resize(300,200)
# Add tabs
self.tabs.addTab(self.tab1,"Submissions")
self.tabs.addTab(self.tab2,"Controls")
self.tabs.addTab(self.tab3, "Add Kit")
# Create first tab
# self.scroller = QWidget()
# self.scroller.layout = QVBoxLayout(self)
# self.scroller.setLayout(self.scroller.layout)
# self.tab1.setMaximumHeight(1000)
self.formwidget = QWidget(self)
self.formlayout = QVBoxLayout(self)
self.formwidget.setLayout(self.formlayout)
self.formwidget.setFixedWidth(300)
self.sheetwidget = QWidget(self)
self.sheetlayout = QVBoxLayout(self)
self.sheetwidget.setLayout(self.sheetlayout)
self.sub_wid = SubmissionsSheet(parent.ctx)
self.sheetlayout.addWidget(self.sub_wid)
self.tab1.layout = QHBoxLayout(self)
self.tab1.setLayout(self.tab1.layout)
# self.tab1.layout.addLayout(self.formlayout)
self.tab1.layout.addWidget(self.formwidget)
self.tab1.layout.addWidget(self.sheetwidget)
# self.tab1.layout.addLayout(self.sheetlayout)
# self.tab1.setWidgetResizable(True)
# self.tab1.setVerticalScrollBar(QScrollBar())
# self.tab1.layout.addWidget(self.scroller)
# self.tab1.setWidget(self.scroller)
# self.tab1.setMinimumHeight(300)
self.webengineview = QWebEngineView()
# data = '''<html>Hello World</html>'''
# self.webengineview.setHtml(data)
self.tab2.layout = QVBoxLayout(self)
self.control_typer = QComboBox()
con_types = get_all_Control_Types_names(ctx=parent.ctx)
self.control_typer.addItems(con_types)
self.tab2.layout.addWidget(self.control_typer)
self.tab2.layout.addWidget(self.webengineview)
self.tab2.setLayout(self.tab2.layout)
# Add tabs to widget
adder = KitAdder(parent_ctx=parent.ctx)
self.tab3.layout = QVBoxLayout(self)
self.tab3.layout.addWidget(adder)
self.tab3.setLayout(self.tab3.layout)
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)
# @pyqtSlot()
# def on_click(self):
# print("\n")
# for currentQTableWidgetItem in self.tableWidget.selectedItems():
# print(currentQTableWidgetItem.row(), currentQTableWidgetItem.column(), currentQTableWidgetItem.text())
# import sys
# from pathlib import Path
# from textual import events
# from textual.app import App, ComposeResult
# from textual.containers import Container, Vertical
# from textual.reactive import var
# from textual.widgets import DirectoryTree, Footer, Header, Input, Label
# from textual.css.query import NoMatches
# sys.path.append(Path(__file__).absolute().parents[1].__str__())
# from backend.excel.parser import SheetParser
# class FormField(Input):
# def on_mount(self):
# self.placeholder = "Value not set."
# def update(self, input:str):
# self.value = input
# class DataBrowser(App):
# """
# File browser input
# """
# CSS_PATH = "static/css/data_browser.css"
# BINDINGS = [
# ("ctrl+f", "toggle_files", "Toggle Files"),
# ("ctrl+q", "quit", "Quit"),
# ]
# show_tree = var(True)
# context = {}
# def watch_show_tree(self, show_tree: bool) -> None:
# """Called when show_tree is modified."""
# self.set_class(show_tree, "-show-tree")
# def compose(self) -> ComposeResult:
# """Compose our UI."""
# if 'directory_path' in self.context:
# path = self.context['directory_path']
# else:
# path = "."
# yield Header()
# yield Container(
# DirectoryTree(path, id="tree-view"),
# Vertical(
# Label("[b]File Name[/b]", classes='box'), FormField(id="file-name", classes='box'),
# # Label("[b]Sample Type[/b]", classes='box'), FormField(id="sample-type", classes='box'),
# id="form-view"
# )
# )
# yield Footer()
# def on_mount(self, event: events.Mount) -> None:
# self.query_one(DirectoryTree).focus()
# def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
# """Called when the user click a file in the directory tree."""
# event.stop()
# sample = SheetParser(Path(event.path), **self.context)
# sample_view = self.query_one("#file-name", FormField)
# # sample_type = self.query_one("#sample-type", FormField)
# sample_view.update(event.path)
# # sample_type.update(sample.sub['sample_type'])
# form_view = self.query_one("#form-view", Vertical)
# if sample.sub != None:
# for var in sample.sub.keys():
# # if var == "sample_type":
# # continue
# try:
# deleter = self.query_one(f"#{var}_label")
# deleter.remove()
# except NoMatches:
# pass
# try:
# deleter = self.query_one(f"#{var}")
# deleter.remove()
# except NoMatches:
# pass
# form_view.mount(Label(var.replace("_", " ").upper(), id=f"{var}_label", classes='box added'))
# form_view.mount(FormField(id=var, classes='box added', value=sample.sub[var]))
# else:
# adds = self.query(".added")
# for add in adds:
# add.remove()
# def action_toggle_files(self) -> None:
# """Called in response to key binding."""
# self.show_tree = not self.show_tree
# if __name__ == "__main__":
# app = DataBrowser()
# app.run()

View File

@@ -0,0 +1,298 @@
from PyQt6.QtWidgets import (
QLabel, QVBoxLayout,
QLineEdit, QComboBox, QDialog,
QDialogButtonBox, QDateEdit, QTableView,
QTextEdit, QSizePolicy, QWidget,
QGridLayout, QPushButton, QSpinBox,
QScrollBar
)
from PyQt6.QtCore import Qt, QDate, QAbstractTableModel
from PyQt6.QtGui import QFontMetrics
from backend.db import get_all_reagenttype_names, submissions_to_df, lookup_submission_by_id, lookup_all_sample_types, create_kit_from_yaml
from jinja2 import Environment, FileSystemLoader
import sys
from pathlib import Path
if getattr(sys, 'frozen', False):
loader_path = Path(sys._MEIPASS).joinpath("files", "templates")
else:
loader_path = Path(__file__).parents[2].joinpath('templates').absolute().__str__()
loader = FileSystemLoader(loader_path)
env = Environment(loader=loader)
class AddReagentQuestion(QDialog):
def __init__(self, reagent_type:str, reagent_lot:str):
super().__init__()
self.setWindowTitle(f"Add {reagent_lot}?")
QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout()
message = QLabel(f"Couldn't find reagent type {reagent_type.replace('_', ' ').title()}: {reagent_lot} in the database.\nWould you like to add it?")
self.layout.addWidget(message)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class AddReagentForm(QDialog):
def __init__(self, ctx:dict, reagent_lot:str|None, reagent_type:str|None):
super().__init__()
if reagent_lot == None:
reagent_lot = ""
self.setWindowTitle("Add Reagent")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
lot_input = QLineEdit()
lot_input.setText(reagent_lot)
exp_input = QDateEdit(calendarPopup=True)
exp_input.setDate(QDate.currentDate())
type_input = QComboBox()
type_input.addItems([item.replace("_", " ").title() for item in get_all_reagenttype_names(ctx=ctx)])
print(f"Trying to find index of {reagent_type}")
try:
reagent_type = reagent_type.replace("_", " ").title()
except AttributeError:
reagent_type = None
index = type_input.findText(reagent_type, Qt.MatchFlag.MatchEndsWith)
if index >= 0:
type_input.setCurrentIndex(index)
self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Lot"))
self.layout.addWidget(lot_input)
self.layout.addWidget(QLabel("Expiry"))
self.layout.addWidget(exp_input)
self.layout.addWidget(QLabel("Type"))
self.layout.addWidget(type_input)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class pandasModel(QAbstractTableModel):
def __init__(self, data):
QAbstractTableModel.__init__(self)
self._data = data
def rowCount(self, parent=None):
return self._data.shape[0]
def columnCount(self, parnet=None):
return self._data.shape[1]
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if index.isValid():
if role == Qt.ItemDataRole.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
return None
def headerData(self, col, orientation, role):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self._data.columns[col]
return None
class SubmissionsSheet(QTableView):
def __init__(self, ctx:dict):
super().__init__()
self.ctx = ctx
self.setData()
self.resizeColumnsToContents()
self.resizeRowsToContents()
# self.clicked.connect(self.test)
self.doubleClicked.connect(self.show_details)
def setData(self):
# horHeaders = []
# for n, key in enumerate(sorted(self.data.keys())):
# horHeaders.append(key)
# for m, item in enumerate(self.data[key]):
# newitem = QTableWidgetItem(item)
# self.setItem(m, n, newitem)
# self.setHorizontalHeaderLabels(horHeaders)
self.data = submissions_to_df(ctx=self.ctx)
self.model = pandasModel(self.data)
self.setModel(self.model)
# self.resize(800,600)
def show_details(self, item):
index=(self.selectionModel().currentIndex())
# print(index)
value=index.sibling(index.row(),0).data()
dlg = SubmissionDetails(ctx=self.ctx, id=value)
if dlg.exec():
pass
class SubmissionDetails(QDialog):
def __init__(self, ctx:dict, id:int) -> None:
super().__init__()
self.setWindowTitle("Submission Details")
data = lookup_submission_by_id(ctx=ctx, id=id)
base_dict = data.to_dict()
base_dict['reagents'] = [item.to_sub_dict() for item in data.reagents]
template = env.get_template("submission_details.txt")
text = template.render(sub=base_dict)
txt_field = QTextEdit(self)
txt_field.setReadOnly(True)
txt_field.document().setPlainText(text)
font = txt_field.document().defaultFont()
fontMetrics = QFontMetrics(font)
textSize = fontMetrics.size(0, txt_field.toPlainText())
w = textSize.width() + 10
h = textSize.height() + 10
txt_field.setMinimumSize(w, h)
txt_field.setMaximumSize(w, h)
txt_field.resize(w, h)
# txt_field.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.MinimumExpanding)
QBtn = QDialogButtonBox.StandardButton.Ok
# self.buttonBox = QDialogButtonBox(QBtn)
# self.buttonBox.accepted.connect(self.accept)
txt_field.setText(text)
self.layout = QVBoxLayout()
self.layout.addWidget(txt_field)
# self.layout.addStretch()
class ReportDatePicker(QDialog):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Select Report Date Range")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
start_date = QDateEdit(calendarPopup=True)
start_date.setDate(QDate.currentDate())
end_date = QDateEdit(calendarPopup=True)
end_date.setDate(QDate.currentDate())
self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(start_date)
self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(end_date)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class KitAdder(QWidget):
def __init__(self, parent_ctx:dict):
super().__init__()
self.ctx = parent_ctx
self.grid = QGridLayout()
self.setLayout(self.grid)
self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1)
self.grid.addWidget(QLabel("Password:"),1,0)
self.grid.addWidget(QLineEdit(),1,1)
self.grid.addWidget(QLabel("Kit Name:"),2,0)
self.grid.addWidget(QLineEdit(),2,1)
self.grid.addWidget(QLabel("Used For Sample Type:"),3,0)
used_for = QComboBox()
used_for.addItems(lookup_all_sample_types(ctx=parent_ctx))
used_for.setEditable(True)
self.grid.addWidget(used_for,3,1)
self.grid.addWidget(QLabel("Cost per run:"),4,0)
cost = QSpinBox()
cost.setMinimum(0)
cost.setMaximum(9999)
self.grid.addWidget(cost,4,1)
self.add_RT_btn = QPushButton("Add Reagent Type")
self.grid.addWidget(self.add_RT_btn)
self.add_RT_btn.clicked.connect(self.add_RT)
self.submit_btn.clicked.connect(self.submit)
def add_RT(self):
maxrow = self.grid.rowCount()
self.grid.addWidget(ReagentTypeForm(parent_ctx=self.ctx), maxrow + 1,0,1,2)
def submit(self):
labels, values, reagents = self.extract_form_info(self)
info = {item[0]:item[1] for item in zip(labels, values)}
print(info)
# info['reagenttypes'] = reagents
# del info['name']
# del info['extension_of_life_(months)']
yml_type = {}
yml_type['password'] = info['password']
used = info['used_for_sample_type'].replace(" ", "_").lower()
yml_type[used] = {}
yml_type[used]['kits'] = {}
yml_type[used]['kits'][info['kit_name']] = {}
yml_type[used]['kits'][info['kit_name']]['cost'] = info['cost_per_run']
yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents
print(yml_type)
create_kit_from_yaml(ctx=self.ctx, exp=yml_type)
def extract_form_info(self, object):
labels = []
values = []
reagents = {}
for item in object.findChildren(QWidget):
print(item.parentWidget())
# if not isinstance(item.parentWidget(), ReagentTypeForm):
match item:
case QLabel():
labels.append(item.text().replace(" ", "_").strip(":").lower())
case QLineEdit():
# ad hoc check to prevent double reporting of qdatedit under lineedit for some reason
if not isinstance(prev_item, QDateEdit) and not isinstance(prev_item, QComboBox) and not isinstance(prev_item, QSpinBox) and not isinstance(prev_item, QScrollBar):
print(f"Previous: {prev_item}")
print(f"Item: {item}, {item.text()}")
values.append(item.text())
case QComboBox():
values.append(item.currentText())
case QDateEdit():
values.append(item.date().toPyDate())
case QSpinBox():
values.append(item.value())
case ReagentTypeForm():
re_labels, re_values, _ = self.extract_form_info(item)
reagent = {item[0]:item[1] for item in zip(re_labels, re_values)}
print(reagent)
# reagent = {reagent['name:']:{'eol':reagent['extension_of_life_(months):']}}
reagents[reagent['name']] = {'eol_ext':int(reagent['extension_of_life_(months)'])}
prev_item = item
return labels, values, reagents
class ReagentTypeForm(QWidget):
def __init__(self, parent_ctx:dict) -> None:
super().__init__()
grid = QGridLayout()
self.setLayout(grid)
grid.addWidget(QLabel("Name:"),0,0)
reagent_getter = QComboBox()
reagent_getter.addItems(get_all_reagenttype_names(ctx=parent_ctx))
reagent_getter.setEditable(True)
grid.addWidget(reagent_getter,0,1)
grid.addWidget(QLabel("Extension of Life (months):"),0,2)
eol = QSpinBox()
eol.setMinimum(0)
grid.addWidget(eol, 0,3)

View File

@@ -0,0 +1,40 @@
Screen {
}
.box {
height: 3;
border: solid green;
}
#tree-view {
display: none;
scrollbar-gutter: stable;
overflow: auto;
width: auto;
height: 100%;
dock: left;
}
DataBrowser.-show-tree #tree-view {
display: block;
max-width: 50%;
}
#code-view {
overflow: auto scroll;
min-width: 100%;
}
#code {
width: auto;
}
FormField {
width: 70%;
height: 3;
padding: 1 2;
background: $primary;
border: $secondary tall;
content-align: center middle;
}

View File

@@ -0,0 +1,9 @@
{% for key, value in sub.items() if key != 'reagents' %}
{{ key }}: {{ value }}
{% endfor %}
Reagents:
{% for item in sub['reagents'] %}
{{ item['type'] }}: {{ item['lot'] }} (EXP: {{ item['expiry'] }})
{% endfor %}