Improved previous sub finding.
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
## 202402.04
|
||||||
|
|
||||||
|
- Addition of comments to gel box.
|
||||||
|
|
||||||
## 202402.01
|
## 202402.01
|
||||||
|
|
||||||
- Addition of gel box for Artic quality control.
|
- Addition of gel box for Artic quality control.
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
|
|||||||
# are written from script.py.mako
|
# are written from script.py.mako
|
||||||
# output_encoding = utf-8
|
# output_encoding = utf-8
|
||||||
|
|
||||||
sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
|
||||||
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db
|
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-demo.db
|
||||||
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db
|
sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db
|
||||||
|
|
||||||
|
|
||||||
[post_write_hooks]
|
[post_write_hooks]
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""adding source plates to Artic submission
|
||||||
|
|
||||||
|
Revision ID: fabf697c721d
|
||||||
|
Revises: 70426df72f80
|
||||||
|
Create Date: 2024-03-06 11:01:34.794411
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'fabf697c721d'
|
||||||
|
down_revision = '70426df72f80'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||||
|
# batch_op.create_unique_constraint("ssa_unique", ['id'])
|
||||||
|
|
||||||
|
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('source_plates', sa.JSON(), nullable=True))
|
||||||
|
|
||||||
|
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
nullable=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
|
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('source_plates')
|
||||||
|
|
||||||
|
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
|
||||||
|
# batch_op.drop_constraint("ssa_unique", type_='unique')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
# Version of the realpython-reader package
|
# Version of the realpython-reader package
|
||||||
__project__ = "submissions"
|
__project__ = "submissions"
|
||||||
__version__ = "202402.4b"
|
__version__ = "202403.1b"
|
||||||
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
|
||||||
__copyright__ = "2022-2024, Government of Canada"
|
__copyright__ = "2022-2024, Government of Canada"
|
||||||
|
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ class BasicSubmission(BaseClass):
|
|||||||
sample_list = self.hitpick_plate()
|
sample_list = self.hitpick_plate()
|
||||||
# logger.debug("Setting background colours")
|
# logger.debug("Setting background colours")
|
||||||
for sample in sample_list:
|
for sample in sample_list:
|
||||||
if sample['positive']:
|
if sample['Positive']:
|
||||||
sample['background_color'] = "#f10f07"
|
sample['background_color'] = "#f10f07"
|
||||||
else:
|
else:
|
||||||
if "colour" in sample.keys():
|
if "colour" in sample.keys():
|
||||||
@@ -288,7 +288,7 @@ class BasicSubmission(BaseClass):
|
|||||||
for column in range(1, plate_columns+1):
|
for column in range(1, plate_columns+1):
|
||||||
for row in range(1, plate_rows+1):
|
for row in range(1, plate_rows+1):
|
||||||
try:
|
try:
|
||||||
well = [item for item in sample_list if item['row'] == row and item['column']==column][0]
|
well = [item for item in sample_list if item['Row'] == row and item['Column']==column][0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
well = dict(name="", row=row, column=column, background_color="#ffffff")
|
well = dict(name="", row=row, column=column, background_color="#ffffff")
|
||||||
output_samples.append(well)
|
output_samples.append(well)
|
||||||
@@ -429,7 +429,8 @@ class BasicSubmission(BaseClass):
|
|||||||
case "reagents":
|
case "reagents":
|
||||||
new_dict[key] = [PydReagent(**reagent) for reagent in value]
|
new_dict[key] = [PydReagent(**reagent) for reagent in value]
|
||||||
case "samples":
|
case "samples":
|
||||||
new_dict[key] = [PydSample(**sample) for sample in dicto['samples']]
|
# samples = {k.lower().replace(" ", "_"):v for k,v in dicto['samples'].items()}
|
||||||
|
new_dict[key] = [PydSample(**{k.lower().replace(" ", "_"):v for k,v in sample.items()}) for sample in dicto['samples']]
|
||||||
case "equipment":
|
case "equipment":
|
||||||
try:
|
try:
|
||||||
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
|
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
|
||||||
@@ -1293,6 +1294,7 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
pcr_info = Column(JSON) #: unstructured output from pcr table logger or user(Artic)
|
pcr_info = Column(JSON) #: unstructured output from pcr table logger or user(Artic)
|
||||||
gel_image = Column(String(64)) #: file name of gel image in zip file
|
gel_image = Column(String(64)) #: file name of gel image in zip file
|
||||||
gel_info = Column(JSON) #: unstructured data from gel.
|
gel_info = Column(JSON) #: unstructured data from gel.
|
||||||
|
source_plates = Column(JSON) #: wastewater plates that samples come from
|
||||||
|
|
||||||
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
|
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
|
||||||
polymorphic_load="inline",
|
polymorphic_load="inline",
|
||||||
@@ -1328,12 +1330,33 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
output['gel_info'] = self.gel_info
|
output['gel_info'] = self.gel_info
|
||||||
output['gel_image'] = self.gel_image
|
output['gel_image'] = self.gel_image
|
||||||
output['dna_core_submission_number'] = self.dna_core_submission_number
|
output['dna_core_submission_number'] = self.dna_core_submission_number
|
||||||
|
output['source_plates'] = self.source_plates
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_abbreviation(cls) -> str:
|
def get_abbreviation(cls) -> str:
|
||||||
return "AR"
|
return "AR"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
|
||||||
|
"""
|
||||||
|
Update submission dictionary with type specific information
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_dict (dict): Input sample dictionary
|
||||||
|
xl (pd.ExcelFile): original xl workbook, used for child classes mostly
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Updated sample dictionary
|
||||||
|
"""
|
||||||
|
input_dict = super().parse_info(input_dict)
|
||||||
|
df = xl.parse("First Strand List", header=None)
|
||||||
|
plates = []
|
||||||
|
for row in [8,9,10]:
|
||||||
|
plates.append(dict(plate=df.iat[row-1, 2], start_sample=df.iat[row-1, 3]))
|
||||||
|
input_dict['source_plates'] = plates
|
||||||
|
return input_dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_samples(cls, input_dict: dict) -> dict:
|
def parse_samples(cls, input_dict: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -1364,8 +1387,11 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: output name
|
str: output name
|
||||||
"""
|
"""
|
||||||
|
# Remove letters.
|
||||||
processed = re.sub(r"[A-Z]", "", input_str)
|
processed = re.sub(r"[A-Z]", "", input_str)
|
||||||
|
# Remove trailing '-' if any
|
||||||
|
processed = processed.strip("-")
|
||||||
try:
|
try:
|
||||||
en_num = re.search(r"\-\d{1}$", processed).group()
|
en_num = re.search(r"\-\d{1}$", processed).group()
|
||||||
processed = rreplace(processed, en_num, "")
|
processed = rreplace(processed, en_num, "")
|
||||||
@@ -1507,18 +1533,22 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
worksheet = input_excel["First Strand List"]
|
worksheet = input_excel["First Strand List"]
|
||||||
samples = cls.query(rsl_number=info['rsl_plate_num']['value']).submission_sample_associations
|
samples = cls.query(rsl_number=info['rsl_plate_num']['value']).submission_sample_associations
|
||||||
samples = sorted(samples, key=attrgetter('column', 'row'))
|
samples = sorted(samples, key=attrgetter('column', 'row'))
|
||||||
source_plates = []
|
try:
|
||||||
first_samples = []
|
source_plates = [item['plate'] for item in info['source_plates']]
|
||||||
for sample in samples:
|
first_samples = [item['start_sample'] for item in info['source_plates']]
|
||||||
sample = sample.sample
|
except:
|
||||||
try:
|
source_plates = []
|
||||||
assoc = [item.submission.rsl_plate_num for item in sample.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
|
first_samples = []
|
||||||
except IndexError:
|
for sample in samples:
|
||||||
logger.error(f"Association not found for {sample}")
|
sample = sample.sample
|
||||||
continue
|
try:
|
||||||
if assoc not in source_plates:
|
assoc = [item.submission.rsl_plate_num for item in sample.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
|
||||||
source_plates.append(assoc)
|
except IndexError:
|
||||||
first_samples.append(sample.ww_processing_num)
|
logger.error(f"Association not found for {sample}")
|
||||||
|
continue
|
||||||
|
if assoc not in source_plates:
|
||||||
|
source_plates.append(assoc)
|
||||||
|
first_samples.append(sample.ww_processing_num)
|
||||||
# Pad list to length of 3
|
# Pad list to length of 3
|
||||||
source_plates += ['None'] * (3 - len(source_plates))
|
source_plates += ['None'] * (3 - len(source_plates))
|
||||||
first_samples += [''] * (3 - len(first_samples))
|
first_samples += [''] * (3 - len(first_samples))
|
||||||
@@ -1573,7 +1603,7 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
Tuple[dict, Template]: (Updated dictionary, Template to be rendered)
|
Tuple[dict, Template]: (Updated dictionary, Template to be rendered)
|
||||||
"""
|
"""
|
||||||
base_dict, template = super().get_details_template(base_dict=base_dict)
|
base_dict, template = super().get_details_template(base_dict=base_dict)
|
||||||
base_dict['excluded'] += ['gel_info', 'gel_image', 'headers', "dna_core_submission_number"]
|
base_dict['excluded'] += ['gel_info', 'gel_image', 'headers', "dna_core_submission_number", "source_plates"]
|
||||||
base_dict['DNA Core ID'] = base_dict['dna_core_submission_number']
|
base_dict['DNA Core ID'] = base_dict['dna_core_submission_number']
|
||||||
check = 'gel_info' in base_dict.keys() and base_dict['gel_info'] != None
|
check = 'gel_info' in base_dict.keys() and base_dict['gel_info'] != None
|
||||||
if check:
|
if check:
|
||||||
@@ -1598,20 +1628,20 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
List[dict]: Updated dictionaries
|
List[dict]: Updated dictionaries
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
|
logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
|
||||||
if backup:
|
# if backup:
|
||||||
output = []
|
output = []
|
||||||
for assoc in self.submission_sample_associations:
|
for assoc in self.submission_sample_associations:
|
||||||
dicto = assoc.to_sub_dict()
|
dicto = assoc.to_sub_dict()
|
||||||
old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
|
old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
|
||||||
try:
|
try:
|
||||||
dicto['plate_name'] = old_sub.rsl_plate_num
|
dicto['plate_name'] = old_sub.rsl_plate_num
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
dicto['plate_name'] = ""
|
dicto['plate_name'] = ""
|
||||||
old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
|
old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
|
||||||
dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
|
dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
|
||||||
output.append(dicto)
|
output.append(dicto)
|
||||||
else:
|
# else:
|
||||||
output = super().adjust_to_dict_samples(backup=False)
|
# output = super().adjust_to_dict_samples(backup=False)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def custom_context_events(self) -> dict:
|
def custom_context_events(self) -> dict:
|
||||||
@@ -1637,9 +1667,15 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
fname = select_open_file(obj=obj, file_extension="jpg")
|
fname = select_open_file(obj=obj, file_extension="jpg")
|
||||||
dlg = GelBox(parent=obj, img_path=fname)
|
dlg = GelBox(parent=obj, img_path=fname)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
self.dna_core_submission_number, img_path, output = dlg.parse_form()
|
self.dna_core_submission_number, img_path, output, comment = dlg.parse_form()
|
||||||
self.gel_image = img_path.name
|
self.gel_image = img_path.name
|
||||||
self.gel_info = output
|
self.gel_info = output
|
||||||
|
dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
|
||||||
|
com = dict(text=comment, name=getuser(), time=dt)
|
||||||
|
if self.comment is not None:
|
||||||
|
self.comment.append(com)
|
||||||
|
else:
|
||||||
|
self.comment = [com]
|
||||||
logger.debug(pformat(self.gel_info))
|
logger.debug(pformat(self.gel_info))
|
||||||
with ZipFile(self.__directory_path__.joinpath("submission_imgs.zip"), 'a') as zipf:
|
with ZipFile(self.__directory_path__.joinpath("submission_imgs.zip"), 'a') as zipf:
|
||||||
# Add a file located at the source_path to the destination within the zip
|
# Add a file located at the source_path to the destination within the zip
|
||||||
@@ -1703,7 +1739,7 @@ class BasicSample(BaseClass):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
return f"<Sample({self.submitter_id})"
|
return f"<Sample({self.submitter_id})"
|
||||||
|
|
||||||
def to_sub_dict(self) -> dict:
|
def to_sub_dict(self, full_data:bool=False) -> dict:
|
||||||
"""
|
"""
|
||||||
gui friendly dictionary, extends parent method.
|
gui friendly dictionary, extends parent method.
|
||||||
|
|
||||||
@@ -1712,8 +1748,10 @@ class BasicSample(BaseClass):
|
|||||||
"""
|
"""
|
||||||
# logger.debug(f"Converting {self} to dict.")
|
# logger.debug(f"Converting {self} to dict.")
|
||||||
sample = {}
|
sample = {}
|
||||||
sample['submitter_id'] = self.submitter_id
|
sample['Submitter ID'] = self.submitter_id
|
||||||
sample['sample_type'] = self.sample_type
|
sample['Sample Type'] = self.sample_type
|
||||||
|
if full_data:
|
||||||
|
sample['submissions'] = [item.to_sub_dict() for item in self.sample_submission_associations]
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
def set_attribute(self, name:str, value):
|
def set_attribute(self, name:str, value):
|
||||||
@@ -1797,6 +1835,41 @@ class BasicSample(BaseClass):
|
|||||||
"""
|
"""
|
||||||
return input_dict
|
return input_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_details_template(cls, base_dict:dict) -> Tuple[dict, Template]:
|
||||||
|
"""
|
||||||
|
Get the details jinja template for the correct class
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_dict (dict): incoming dictionary of Submission fields
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
|
||||||
|
"""
|
||||||
|
base_dict['excluded'] = ['submissions', 'excluded']
|
||||||
|
env = jinja_template_loading()
|
||||||
|
temp_name = f"{cls.__name__.lower()}_details.html"
|
||||||
|
logger.debug(f"Returning template: {temp_name}")
|
||||||
|
try:
|
||||||
|
template = env.get_template(temp_name)
|
||||||
|
except TemplateNotFound as e:
|
||||||
|
logger.error(f"Couldn't find template {e}")
|
||||||
|
template = env.get_template("basicsample_details.html")
|
||||||
|
return base_dict, template
|
||||||
|
|
||||||
|
def show_details(self, obj):
|
||||||
|
"""
|
||||||
|
Creates Widget for showing sample details.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj (_type_): parent widget
|
||||||
|
"""
|
||||||
|
logger.debug("Hello from details")
|
||||||
|
from frontend.widgets.sample_details import SampleDetails
|
||||||
|
dlg = SampleDetails(parent=obj, samp=self)
|
||||||
|
if dlg.exec():
|
||||||
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@setup_lookup
|
||||||
def query(cls,
|
def query(cls,
|
||||||
@@ -1896,18 +1969,18 @@ class WastewaterSample(BasicSample):
|
|||||||
polymorphic_load="inline",
|
polymorphic_load="inline",
|
||||||
inherit_condition=(id == BasicSample.id))
|
inherit_condition=(id == BasicSample.id))
|
||||||
|
|
||||||
def to_sub_dict(self) -> dict:
|
def to_sub_dict(self, full_data:bool=False) -> dict:
|
||||||
"""
|
"""
|
||||||
gui friendly dictionary, extends parent method.
|
gui friendly dictionary, extends parent method.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
||||||
"""
|
"""
|
||||||
sample = super().to_sub_dict()
|
sample = super().to_sub_dict(full_data=full_data)
|
||||||
sample['ww_processing_num'] = self.ww_processing_num
|
sample['WW Processing Number'] = self.ww_processing_num
|
||||||
sample['sample_location'] = self.sample_location
|
sample['Sample Location'] = self.sample_location
|
||||||
sample['received_date'] = self.received_date
|
sample['Received Date'] = self.received_date
|
||||||
sample['collection_date'] = self.collection_date
|
sample['Collection Date'] = self.collection_date
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1944,9 +2017,14 @@ class WastewaterSample(BasicSample):
|
|||||||
|
|
||||||
def get_previous_ww_submission(self, current_artic_submission:WastewaterArtic):
|
def get_previous_ww_submission(self, current_artic_submission:WastewaterArtic):
|
||||||
# assocs = [assoc for assoc in self.sample_submission_associations if assoc.submission.submission_type_name=="Wastewater"]
|
# assocs = [assoc for assoc in self.sample_submission_associations if assoc.submission.submission_type_name=="Wastewater"]
|
||||||
subs = self.submissions[:self.submissions.index(current_artic_submission)]
|
# subs = self.submissions[:self.submissions.index(current_artic_submission)]
|
||||||
subs = [sub for sub in subs if sub.submission_type_name=="Wastewater"]
|
try:
|
||||||
logger.debug(f"Submissions up to current artic submission: {subs}")
|
plates = [item['plate'] for item in current_artic_submission.source_plates]
|
||||||
|
except TypeError as e:
|
||||||
|
logger.error(f"source_plates must not be present")
|
||||||
|
plates = [item.rsl_plate_num for item in self.submissions[:self.submissions.index(current_artic_submission)]]
|
||||||
|
subs = [sub for sub in self.submissions if sub.rsl_plate_num in plates]
|
||||||
|
logger.debug(f"Submissions: {subs}")
|
||||||
try:
|
try:
|
||||||
return subs[-1]
|
return subs[-1]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@@ -1964,17 +2042,17 @@ class BacterialCultureSample(BasicSample):
|
|||||||
polymorphic_load="inline",
|
polymorphic_load="inline",
|
||||||
inherit_condition=(id == BasicSample.id))
|
inherit_condition=(id == BasicSample.id))
|
||||||
|
|
||||||
def to_sub_dict(self) -> dict:
|
def to_sub_dict(self, full_data:bool=False) -> dict:
|
||||||
"""
|
"""
|
||||||
gui friendly dictionary, extends parent method.
|
gui friendly dictionary, extends parent method.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
||||||
"""
|
"""
|
||||||
sample = super().to_sub_dict()
|
sample = super().to_sub_dict(full_data=full_data)
|
||||||
sample['name'] = self.submitter_id
|
sample['Name'] = self.submitter_id
|
||||||
sample['organism'] = self.organism
|
sample['Organism'] = self.organism
|
||||||
sample['concentration'] = self.concentration
|
sample['Concentration'] = self.concentration
|
||||||
if self.control != None:
|
if self.control != None:
|
||||||
sample['colour'] = [0,128,0]
|
sample['colour'] = [0,128,0]
|
||||||
sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}"
|
sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}"
|
||||||
@@ -2038,16 +2116,16 @@ class SubmissionSampleAssociation(BaseClass):
|
|||||||
# Get sample info
|
# Get sample info
|
||||||
# logger.debug(f"Running {self.__repr__()}")
|
# logger.debug(f"Running {self.__repr__()}")
|
||||||
sample = self.sample.to_sub_dict()
|
sample = self.sample.to_sub_dict()
|
||||||
sample['name'] = self.sample.submitter_id
|
sample['Name'] = self.sample.submitter_id
|
||||||
sample['row'] = self.row
|
sample['Row'] = self.row
|
||||||
sample['column'] = self.column
|
sample['Column'] = self.column
|
||||||
try:
|
try:
|
||||||
sample['well'] = f"{row_map[self.row]}{self.column}"
|
sample['Well'] = f"{row_map[self.row]}{self.column}"
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
logger.error(f"Unable to find row {self.row} in row_map.")
|
logger.error(f"Unable to find row {self.row} in row_map.")
|
||||||
sample['well'] = None
|
sample['Well'] = None
|
||||||
sample['plate_name'] = self.submission.rsl_plate_num
|
sample['Plate Name'] = self.submission.rsl_plate_num
|
||||||
sample['positive'] = False
|
sample['Positive'] = False
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
def to_hitpick(self) -> dict|None:
|
def to_hitpick(self) -> dict|None:
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ class PydEquipment(BaseModel, extra='ignore'):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def make_empty_list(cls, value):
|
def make_empty_list(cls, value):
|
||||||
# logger.debug(f"Pydantic value: {value}")
|
# logger.debug(f"Pydantic value: {value}")
|
||||||
|
value = convert_nans_to_nones(value)
|
||||||
if value == None:
|
if value == None:
|
||||||
value = ['']
|
value = ['']
|
||||||
if len(value)==0:
|
if len(value)==0:
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
Gel box for artic quality control
|
Gel box for artic quality control
|
||||||
"""
|
"""
|
||||||
from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout,
|
from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout,
|
||||||
QLabel, QLineEdit, QDialogButtonBox
|
QLabel, QLineEdit, QDialogButtonBox,
|
||||||
|
QTextEdit
|
||||||
)
|
)
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
@@ -44,7 +45,8 @@ class GelBox(QDialog):
|
|||||||
# creating image view object
|
# creating image view object
|
||||||
self.imv = pg.ImageView()
|
self.imv = pg.ImageView()
|
||||||
img = np.array(Image.open(self.img_path).rotate(-90).transpose(Image.FLIP_LEFT_RIGHT))
|
img = np.array(Image.open(self.img_path).rotate(-90).transpose(Image.FLIP_LEFT_RIGHT))
|
||||||
self.imv.setImage(img)#, xvals=np.linspace(1., 3., data.shape[0]))
|
self.imv.setImage(img, scale=None)#, xvals=np.linspace(1., 3., data.shape[0]))
|
||||||
|
|
||||||
layout = QGridLayout()
|
layout = QGridLayout()
|
||||||
layout.addWidget(QLabel("DNA Core Submission Number"),0,1)
|
layout.addWidget(QLabel("DNA Core Submission Number"),0,1)
|
||||||
self.core_number = QLineEdit()
|
self.core_number = QLineEdit()
|
||||||
@@ -59,7 +61,7 @@ class GelBox(QDialog):
|
|||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
layout.addWidget(self.buttonBox, 22, 5, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
|
layout.addWidget(self.buttonBox, 23, 1, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def parse_form(self) -> Tuple[str, str|Path, list]:
|
def parse_form(self) -> Tuple[str, str|Path, list]:
|
||||||
@@ -70,14 +72,14 @@ class GelBox(QDialog):
|
|||||||
Tuple[str, str|Path, list]: output values
|
Tuple[str, str|Path, list]: output values
|
||||||
"""
|
"""
|
||||||
dna_core_submission_number = self.core_number.text()
|
dna_core_submission_number = self.core_number.text()
|
||||||
return dna_core_submission_number, self.img_path, self.form.parse_form()
|
values, comment = self.form.parse_form()
|
||||||
|
return dna_core_submission_number, self.img_path, values, comment
|
||||||
|
|
||||||
class ControlsForm(QWidget):
|
class ControlsForm(QWidget):
|
||||||
|
|
||||||
def __init__(self, parent) -> None:
|
def __init__(self, parent) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.layout = QGridLayout()
|
self.layout = QGridLayout()
|
||||||
|
|
||||||
columns = []
|
columns = []
|
||||||
rows = []
|
rows = []
|
||||||
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
|
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
|
||||||
@@ -98,6 +100,11 @@ class ControlsForm(QWidget):
|
|||||||
widge.setText("Neg")
|
widge.setText("Neg")
|
||||||
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
|
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
|
||||||
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
|
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
|
||||||
|
self.layout.addWidget(QLabel("Comments:"), 0,5,1,1)
|
||||||
|
self.comment_field = QTextEdit(self)
|
||||||
|
self.comment_field.setFixedHeight(50)
|
||||||
|
self.layout.addWidget(self.comment_field, 1,5,4,1)
|
||||||
|
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def parse_form(self) -> List[dict]:
|
def parse_form(self) -> List[dict]:
|
||||||
@@ -118,4 +125,4 @@ class ControlsForm(QWidget):
|
|||||||
if label[0] not in [item['name'] for item in output]:
|
if label[0] not in [item['name'] for item in output]:
|
||||||
output.append(dicto)
|
output.append(dicto)
|
||||||
logger.debug(pformat(output))
|
logger.debug(pformat(output))
|
||||||
return output
|
return output, self.comment_field.toPlainText()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox,
|
from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox,
|
||||||
QDialogButtonBox, QTextEdit)
|
QDialogButtonBox, QTextEdit)
|
||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtWebChannel import QWebChannel
|
||||||
from backend.db.models import BasicSubmission
|
from PyQt6.QtCore import Qt, pyqtSlot
|
||||||
|
|
||||||
|
from backend.db.models import BasicSubmission, BasicSample
|
||||||
from tools import check_if_app
|
from tools import check_if_app
|
||||||
from .functions import select_save_file
|
from .functions import select_save_file
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -31,7 +33,7 @@ class SubmissionDetails(QDialog):
|
|||||||
self.app = parent.parent().parent().parent().parent().parent().parent()
|
self.app = parent.parent().parent().parent().parent().parent().parent()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self.app = None
|
self.app = None
|
||||||
self.setWindowTitle("Submission Details")
|
self.setWindowTitle(f"Submission Details - {sub.rsl_plate_num}")
|
||||||
# create scrollable interior
|
# create scrollable interior
|
||||||
interior = QScrollArea()
|
interior = QScrollArea()
|
||||||
interior.setParent(self)
|
interior.setParent(self)
|
||||||
@@ -46,19 +48,34 @@ class SubmissionDetails(QDialog):
|
|||||||
self.base_dict['platemap'] = sub.make_plate_map()
|
self.base_dict['platemap'] = sub.make_plate_map()
|
||||||
self.base_dict, self.template = sub.get_details_template(base_dict=self.base_dict)
|
self.base_dict, self.template = sub.get_details_template(base_dict=self.base_dict)
|
||||||
self.html = self.template.render(sub=self.base_dict)
|
self.html = self.template.render(sub=self.base_dict)
|
||||||
webview = QWebEngineView()
|
self.webview = QWebEngineView(parent=self)
|
||||||
webview.setMinimumSize(900, 500)
|
self.webview.setMinimumSize(900, 500)
|
||||||
webview.setMaximumSize(900, 500)
|
self.webview.setMaximumSize(900, 500)
|
||||||
webview.setHtml(self.html)
|
self.webview.setHtml(self.html)
|
||||||
self.layout = QVBoxLayout()
|
self.layout = QVBoxLayout()
|
||||||
interior.resize(900, 500)
|
interior.resize(900, 500)
|
||||||
interior.setWidget(webview)
|
interior.setWidget(self.webview)
|
||||||
self.setFixedSize(900, 500)
|
self.setFixedSize(900, 500)
|
||||||
# button to export a pdf version
|
# button to export a pdf version
|
||||||
btn = QPushButton("Export PDF")
|
btn = QPushButton("Export PDF")
|
||||||
btn.setParent(self)
|
btn.setParent(self)
|
||||||
btn.setFixedWidth(900)
|
btn.setFixedWidth(900)
|
||||||
btn.clicked.connect(self.export)
|
btn.clicked.connect(self.export)
|
||||||
|
# setup channel
|
||||||
|
self.channel = QWebChannel()
|
||||||
|
self.channel.registerObject('backend', self)
|
||||||
|
self.webview.page().setWebChannel(self.channel)
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def sample_details(self, sample):
|
||||||
|
# print(f"{string} is in row {row}, column {column}")
|
||||||
|
# self.webview.setHtml(f"<html><body><br><br>{sample}</body></html>")
|
||||||
|
sample = BasicSample.query(submitter_id=sample)
|
||||||
|
base_dict = sample.to_sub_dict(full_data=True)
|
||||||
|
base_dict, template = sample.get_details_template(base_dict=base_dict)
|
||||||
|
html = template.render(sample=base_dict)
|
||||||
|
self.webview.setHtml(html)
|
||||||
|
# sample.show_details(obj=self)
|
||||||
|
|
||||||
def export(self):
|
def export(self):
|
||||||
"""
|
"""
|
||||||
@@ -130,4 +147,4 @@ class SubmissionComment(QDialog):
|
|||||||
full_comment = [{"name":commenter, "time": dt, "text": comment}]
|
full_comment = [{"name":commenter, "time": dt, "text": comment}]
|
||||||
logger.debug(f"Full comment: {full_comment}")
|
logger.debug(f"Full comment: {full_comment}")
|
||||||
return full_comment
|
return full_comment
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ class SubmissionsSheet(QTableView):
|
|||||||
Args:
|
Args:
|
||||||
event (_type_): the item of interest
|
event (_type_): the item of interest
|
||||||
"""
|
"""
|
||||||
|
# logger.debug(event().__dict__)
|
||||||
id = self.selectionModel().currentIndex()
|
id = self.selectionModel().currentIndex()
|
||||||
id = id.sibling(id.row(),0).data()
|
id = id.sibling(id.row(),0).data()
|
||||||
submission = BasicSubmission.query(id=id)
|
submission = BasicSubmission.query(id=id)
|
||||||
|
|||||||
54
src/submissions/templates/basicsample_details.html
Normal file
54
src/submissions/templates/basicsample_details.html
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
/* Tooltip container */
|
||||||
|
.tooltip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip text */
|
||||||
|
.tooltip .tooltiptext {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 120px;
|
||||||
|
background-color: black;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
/* Position the tooltip text - see examples below! */
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show the tooltip text when you mouse over the tooltip container */
|
||||||
|
.tooltip:hover .tooltiptext {
|
||||||
|
visibility: visible;
|
||||||
|
font-size: large;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title>Sample Details for {{ sample['Submitter ID'] }}</title>
|
||||||
|
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block body %}
|
||||||
|
<h2><u>Sample Details for {{ sample['Submitter ID'] }}</u></h2>
|
||||||
|
<p>{% for key, value in sample.items() if key not in sample['excluded'] %}
|
||||||
|
<b>{{ key }}: </b>{{ value }}<br>
|
||||||
|
{% endfor %}</p>
|
||||||
|
{% if sample['submissions'] %}<h2>Submissions:</h2>
|
||||||
|
{% for submission in sample['submissions'] %}
|
||||||
|
<p>{{ submission['Plate Name'] }}: {{ submission['Well'] }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
<title>Submission Details for {{ sub['Plate Number'] }}</title>
|
||||||
|
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
{% if sub['samples'] %}
|
{% if sub['samples'] %}
|
||||||
<h3><u>Samples:</u></h3>
|
<h3><u>Samples:</u></h3>
|
||||||
<p>{% for item in sub['samples'] %}
|
<p>{% for item in sub['samples'] %}
|
||||||
<b>{{ item['well'] }}:</b> {% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br> ') }}){% else %} {{ item['name']|replace('\n\t', '<br> ') }}{% endif %}<br>
|
<b>{{ item['Well'] }}:</b> {% if item['Organism'] %} {{ item['Name'] }} - ({{ item['Organism']|replace('\n\t', '<br> ') }}){% else %} {{ item['Name']|replace('\n\t', '<br> ') }}{% endif %}<br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sub['controls'] %}
|
{% if sub['controls'] %}
|
||||||
@@ -116,4 +117,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
<script>
|
||||||
|
var backend;
|
||||||
|
new QWebChannel(qt.webChannelTransport, function (channel) {
|
||||||
|
backend = channel.objects.backend;
|
||||||
|
});
|
||||||
|
{% for sample in sub['samples'] %}
|
||||||
|
document.getElementById("{{sample['Submitter ID']}}").addEventListener("dblclick", function(){
|
||||||
|
backend.sample_details("{{ sample['Submitter ID'] }}");
|
||||||
|
});
|
||||||
|
{% endfor %}
|
||||||
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<div class="gallery" style="display: grid;grid-template-columns: repeat({{ PLATE_COLUMNS }}, 7.5vw);grid-template-rows: repeat({{ PLATE_ROWS }}, 7.5vw);grid-gap: 2px;">
|
<div class="gallery" style="display: grid;grid-template-columns: repeat({{ PLATE_COLUMNS }}, 7.5vw);grid-template-rows: repeat({{ PLATE_ROWS }}, 7.5vw);grid-gap: 2px;">
|
||||||
{% for sample in samples %}
|
{% for sample in samples %}
|
||||||
<div class="well" style="background-color: {{sample['background_color']}};
|
<div class="well" id="{{sample['Submitter ID']}}" style="background-color: {{sample['background_color']}};
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
grid-column-start: {{sample['column']}};
|
grid-column-start: {{sample['Column']}};
|
||||||
grid-column-end: {{sample['column']}};
|
grid-column-end: {{sample['Column']}};
|
||||||
grid-row-start: {{sample['row']}};
|
grid-row-start: {{sample['Row']}};
|
||||||
grid-row-end: {{sample['row']}};
|
grid-row-end: {{sample['Row']}};
|
||||||
display: flex;
|
display: flex;
|
||||||
">
|
">
|
||||||
<div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['name'] }}
|
<div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['Name'] }}
|
||||||
<span class="tooltiptext">{{ sample['tooltip'] }}</span>
|
<span class="tooltiptext">{{ sample['tooltip'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Sample name: {{ fields['submitter_id'] }}<br>
|
Sample name: {{ fields['Submitter ID'] }}<br>
|
||||||
{% if fields['organism'] %}Organism: {{ fields['organism'] }}<br>{% endif %}
|
{% if fields['Organism'] %}Organism: {{ fields['Organism'] }}<br>{% endif %}
|
||||||
{% if fields['concentration'] %}Concentration: {{ fields['concentration'] }}<br>{% endif %}
|
{% if fields['Concentration'] %}Concentration: {{ fields['Concentration'] }}<br>{% endif %}
|
||||||
Well: {{ fields['well'] }}<!--{{ fields['column'] }}-->
|
Well: {{ fields['Well'] }}<!--{{ fields['column'] }}-->
|
||||||
@@ -116,37 +116,7 @@ def check_regex_match(pattern:str, check:str) -> bool:
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
|
# Settings
|
||||||
|
|
||||||
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)
|
|
||||||
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())
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
"""
|
"""
|
||||||
@@ -303,6 +273,26 @@ def get_config(settings_path: Path|str|None=None) -> Settings:
|
|||||||
settings = yaml.load(stream, Loader=yaml.Loader)
|
settings = yaml.load(stream, Loader=yaml.Loader)
|
||||||
return Settings(**settings)
|
return Settings(**settings)
|
||||||
|
|
||||||
|
# Logging formatters
|
||||||
|
|
||||||
|
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)
|
||||||
|
rtv=handlers.RotatingFileHandler._open(self)
|
||||||
|
os.umask(prevumask)
|
||||||
|
return rtv
|
||||||
|
|
||||||
class CustomFormatter(logging.Formatter):
|
class CustomFormatter(logging.Formatter):
|
||||||
|
|
||||||
grey = "\x1b[38;20m"
|
grey = "\x1b[38;20m"
|
||||||
@@ -326,6 +316,20 @@ class CustomFormatter(logging.Formatter):
|
|||||||
formatter = logging.Formatter(log_fmt)
|
formatter = logging.Formatter(log_fmt)
|
||||||
return formatter.format(record)
|
return formatter.format(record)
|
||||||
|
|
||||||
|
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 setup_logger(verbosity:int=3):
|
def setup_logger(verbosity:int=3):
|
||||||
"""
|
"""
|
||||||
Set logger levels using settings.
|
Set logger levels using settings.
|
||||||
|
|||||||
Reference in New Issue
Block a user