Compare commits

...

2 Commits

Author SHA1 Message Date
lwark
4d70d751ca Qubit results parsing complete. 2025-09-23 08:59:04 -05:00
lwark
39d20bbc22 Qubit results parsing complete. 2025-09-23 08:57:40 -05:00
15 changed files with 117 additions and 53 deletions

View File

@@ -1,10 +1,15 @@
# 202509.04
- Qubit results parsing complete.
# 202509.03 # 202509.03
- Sortable headers in treeview. - Sortable headers in treeview.
- Added gitea remote.
# 202509.02 # 202509.02
- First Useable updated version. - First usable updated version.
# 202504.04 # 202504.04
@@ -12,7 +17,7 @@
# 202504.03 # 202504.03
- Split Concentration controls on the chart so they are individually selectable. - Split Concentration controls on the chart, so they are individually selectable.
# 202504.02 # 202504.02
@@ -315,7 +320,7 @@
## 202307.03 ## 202307.03
- Auto-filling of some empty cells in Excel file. - Autofilling of some empty cells in Excel file.
- Better pydantic validations of missing data. - Better pydantic validations of missing data.
## 202307.02 ## 202307.02

View File

@@ -1,5 +1,5 @@
- [ ] Add in database objects for rsl_run (submission -> run), procedure (run -> procedure), many more things will likely be associated with procedure. - [x] Add in database objects for rsl_run (submission -> run), procedure (run -> procedure), many more things will likely be associated with procedure.
- [ ] Add in database object for client submission. - [x] Add in database object for client submission.
- [ ] Add arbitrary pipette addition to equipment UI. - [ ] Add arbitrary pipette addition to equipment UI.
- [ ] transfer details template rendering fully into sql objects - [ ] transfer details template rendering fully into sql objects
- [x] Add in connecting links for tips. - [x] Add in connecting links for tips.

View File

@@ -18,7 +18,7 @@ from sqlalchemy.exc import ArgumentError
from typing import Any, List, ClassVar from typing import Any, List, ClassVar
from pathlib import Path from pathlib import Path
from sqlalchemy.orm.relationships import _RelationshipDeclared from sqlalchemy.orm.relationships import _RelationshipDeclared
from tools import report_result, list_sort_dict, jinja_template_loading, Report, Result, ctx from tools import report_result, list_sort_dict, jinja_template_loading, Report, Alert, ctx
# NOTE: Load testing environment # NOTE: Load testing environment
if 'pytest' in sys.modules: if 'pytest' in sys.modules:
@@ -364,7 +364,7 @@ class BaseClass(Base):
logger.error(f"Error message: {type(e)}") logger.error(f"Error message: {type(e)}")
logger.error(pformat(self.__dict__)) logger.error(pformat(self.__dict__))
self.__database_session__.rollback() self.__database_session__.rollback()
report.add_result(Result(msg=e, status="Critical")) report.add_result(Alert(msg=e, status="Critical"))
return report return report
@property @property

View File

@@ -4,13 +4,14 @@ All kittype and reagent related models
from __future__ import annotations from __future__ import annotations
import zipfile, logging, re, numpy as np import zipfile, logging, re, numpy as np
from operator import itemgetter from operator import itemgetter
from pathlib import Path
from pprint import pformat from pprint import pformat
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, func from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, func
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, validates, Query, declared_attr from sqlalchemy.orm import relationship, validates, Query, declared_attr
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \ from tools import check_authorization, setup_lookup, Report, Alert, check_regex_match, timezone, \
jinja_template_loading, flatten_list jinja_template_loading, flatten_list
from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING
from . import BaseClass, ClientLab, LogMixin from . import BaseClass, ClientLab, LogMixin
@@ -926,10 +927,13 @@ class Procedure(BaseClass):
logger.info(f"Add Results! {resultstype_name}") logger.info(f"Add Results! {resultstype_name}")
from backend.managers import results from backend.managers import results
results_manager = getattr(results, f"{resultstype_name}Manager") results_manager = getattr(results, f"{resultstype_name}Manager")
rs = results_manager(procedure=self, parent=obj) rs = results_manager(procedure=self, parent=obj, fname=Path("C:\\Users\lwark\Documents\Submission_Forms\QubitData_18-09-2025_13-43-53.csv"))
procedure = rs.procedure_to_pydantic() procedure = rs.procedure_to_pydantic()
samples = rs.samples_to_pydantic() samples = rs.samples_to_pydantic()
if procedure:
procedure_sql = procedure.to_sql() procedure_sql = procedure.to_sql()
else:
return
procedure_sql.save() procedure_sql.save()
for sample in samples: for sample in samples:
sample_sql = sample.to_sql() sample_sql = sample.to_sql()

View File

@@ -18,7 +18,8 @@ from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
from tools import setup_lookup, jinja_template_loading, create_holidays_for_year, check_dictionary_inclusion_equality, is_power_user from tools import (setup_lookup, jinja_template_loading, create_holidays_for_year,
check_dictionary_inclusion_equality, is_power_user, row_map)
from datetime import datetime, date from datetime import datetime, date
from typing import List, Literal, Generator, TYPE_CHECKING from typing import List, Literal, Generator, TYPE_CHECKING
from pathlib import Path from pathlib import Path
@@ -1865,6 +1866,16 @@ class ProcedureSampleAssociation(BaseClass):
results = relationship("Results", back_populates="sampleprocedureassociation") #: associated results results = relationship("Results", back_populates="sampleprocedureassociation") #: associated results
@property
def well(self):
if self.row > 0:
if self.column > 0:
return f"{row_map[self.row]}{self.column}"
else:
return self.row
else:
return None
@classmethod @classmethod
def query(cls, sample: Sample | str | None = None, procedure: Procedure | str | None = None, limit: int = 0, def query(cls, sample: Sample | str | None = None, procedure: Procedure | str | None = None, limit: int = 0,
**kwargs): **kwargs):

View File

@@ -2,11 +2,13 @@
Default Parser archetypes. Default Parser archetypes.
""" """
from __future__ import annotations from __future__ import annotations
import logging, re import logging, re, csv
from pathlib import Path from pathlib import Path
from pprint import pformat
from typing import Generator, TYPE_CHECKING from typing import Generator, TYPE_CHECKING
from openpyxl.cell import MergedCell from openpyxl.cell import MergedCell
from openpyxl.reader.excel import load_workbook from openpyxl.reader.excel import load_workbook
from openpyxl.workbook import Workbook
from pandas import DataFrame from pandas import DataFrame
from backend.validators import pydant from backend.validators import pydant
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -44,6 +46,8 @@ class DefaultParser(object):
**kwargs (): **kwargs ():
""" """
logger.info(f"\n\nHello from {self.__class__.__name__}\n\n") logger.info(f"\n\nHello from {self.__class__.__name__}\n\n")
if isinstance(filepath, str):
filepath = Path(filepath)
self.filepath = filepath self.filepath = filepath
self.proceduretype = proceduretype self.proceduretype = proceduretype
try: try:
@@ -58,13 +62,27 @@ class DefaultParser(object):
self.sheet = sheet self.sheet = sheet
if not start_row: if not start_row:
start_row = self.__class__.start_row start_row = self.__class__.start_row
if self.filepath.suffix == ".xslx":
self.workbook = load_workbook(self.filepath, data_only=True) self.workbook = load_workbook(self.filepath, data_only=True)
self.worksheet = self.workbook[self.sheet] self.worksheet = self.workbook[self.sheet]
elif self.filepath.suffix == ".csv":
self.workbook, self.worksheet = self.csv2xlsx(self.filepath)
self.start_row = self.delineate_start_row(start_row=start_row) self.start_row = self.delineate_start_row(start_row=start_row)
self.end_row = self.delineate_end_row(start_row=self.start_row) self.end_row = self.delineate_end_row(start_row=self.start_row)
@classmethod
def csv2xlsx(cls, filepath):
wb = Workbook()
ws = wb.active
with open(filepath, "r") as f:
reader = csv.reader(f, delimiter=",")
for row in reader:
ws.append(row)
return wb, ws
def to_pydantic(self): def to_pydantic(self):
data = self.parsed_info data = self.parsed_info
logger.debug(f"Data for {self.__class__.__name__}: {pformat(data)}")
data['filepath'] = self.filepath data['filepath'] = self.filepath
return self._pyd_object(**data) return self._pyd_object(**data)
@@ -85,7 +103,7 @@ class DefaultParser(object):
for iii, row in enumerate(self.worksheet.iter_rows(min_row=start_row), start=start_row): for iii, row in enumerate(self.worksheet.iter_rows(min_row=start_row), start=start_row):
if all([item.value is None for item in row]): if all([item.value is None for item in row]):
return iii return iii
return self.worksheet.max_row return self.worksheet.max_row + 1
class DefaultKEYVALUEParser(DefaultParser): class DefaultKEYVALUEParser(DefaultParser):

View File

@@ -12,12 +12,22 @@ logger = logging.getLogger(f"submissions.{__name__}")
class DefaultResultsInfoParser(DefaultKEYVALUEParser): class DefaultResultsInfoParser(DefaultKEYVALUEParser):
pyd_name = "PydResults" pyd_name = "PydResults"
def __init__(self, filepath: Path | str, proceduretype: "ProcedureType" | None = None, def __init__(self, filepath: Path | str, results_type: str, proceduretype: "ProcedureType" | None = None,
results_type: str | None = "PCR", *args, **kwargs): *args, **kwargs):
if results_type: if results_type:
self.results_type = results_type self.results_type = results_type
try:
sheet = proceduretype.allowed_result_methods[results_type]['info']['sheet'] sheet = proceduretype.allowed_result_methods[results_type]['info']['sheet']
except KeyError:
sheet = 1
if "start_row" not in kwargs:
try:
start_row = proceduretype.allowed_result_methods[results_type]['info']['start_row'] start_row = proceduretype.allowed_result_methods[results_type]['info']['start_row']
except KeyError:
start_row = 1
else:
start_row = kwargs.pop('start_row')
# start_row = proceduretype.allowed_result_methods[results_type]['info']['start_row']
super().__init__(filepath=filepath, proceduretype=proceduretype, sheet=sheet, start_row=start_row, *args, super().__init__(filepath=filepath, proceduretype=proceduretype, sheet=sheet, start_row=start_row, *args,
**kwargs) **kwargs)
@@ -25,14 +35,24 @@ class DefaultResultsInfoParser(DefaultKEYVALUEParser):
class DefaultResultsSampleParser(DefaultTABLEParser): class DefaultResultsSampleParser(DefaultTABLEParser):
pyd_name = "PydResults" pyd_name = "PydResults"
def __init__(self, filepath: Path | str, proceduretype: "ProcedureType" | None = None, def __init__(self, filepath: Path | str, results_type: str, proceduretype: "ProcedureType" | None = None,
results_type: str | None = "PCR", *args, **kwargs): *args, **kwargs):
if results_type: if results_type:
self.results_type = results_type self.results_type = results_type
try:
sheet = proceduretype.allowed_result_methods[results_type]['sample']['sheet'] sheet = proceduretype.allowed_result_methods[results_type]['sample']['sheet']
except KeyError:
sheet = 1
if "start_row" not in kwargs:
try:
start_row = proceduretype.allowed_result_methods[results_type]['sample']['start_row'] start_row = proceduretype.allowed_result_methods[results_type]['sample']['start_row']
except KeyError:
start_row = 1
else:
start_row = kwargs.pop('start_row')
super().__init__(filepath=filepath, proceduretype=proceduretype, sheet=sheet, start_row=start_row, *args, super().__init__(filepath=filepath, proceduretype=proceduretype, sheet=sheet, start_row=start_row, *args,
**kwargs) **kwargs)
from .pcr_results_parser import PCRInfoParser, PCRSampleParser from .pcr_results_parser import PCRInfoParser, PCRSampleParser
from .qubit_results_parser import QubitInfoParser, QubitSampleParser

View File

@@ -17,15 +17,19 @@ logger = logging.getLogger(f"submission.{__name__}")
class DefaultResultsManager(DefaultManager): class DefaultResultsManager(DefaultManager):
def __init__(self, procedure: Procedure, parent, fname: Path | str | None = None): def __init__(self, procedure: Procedure, parent, fname: Path | str | None = None, extension: str|None="xlsx"):
self.procedure = procedure self.procedure = procedure
if not fname: if not fname:
self.fname = select_open_file(file_extension="xlsx", obj=get_application_from_parent(parent)) fname = select_open_file(file_extension=extension, obj=get_application_from_parent(parent))
elif isinstance(fname, str): elif isinstance(fname, str):
self.fname = Path(fname) fname = Path(fname)
self.fname = fname
def procedure_to_pydantic(self) -> PydResults: def procedure_to_pydantic(self) -> PydResults:
logger.debug(f"Info parser: {self.info_parser}")
info = self.info_parser.to_pydantic() info = self.info_parser.to_pydantic()
if info:
info.parent = self.procedure info.parent = self.procedure
return info return info
@@ -34,3 +38,4 @@ class DefaultResultsManager(DefaultManager):
return sample return sample
from .pcr_results_manager import PCRManager from .pcr_results_manager import PCRManager
from .qubit_results_manager import QubitManager

View File

@@ -12,7 +12,7 @@ from typing import List, Tuple, Literal, Generator
from types import GeneratorType from types import GeneratorType
from . import RSLNamer from . import RSLNamer
from pathlib import Path from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone, sort_dict_by_list, row_keys, flatten_list from tools import check_not_nan, convert_nans_to_nones, Report, Alert, timezone, sort_dict_by_list, row_keys, flatten_list
from backend.db import models from backend.db import models
from backend.db.models import * from backend.db.models import *
from sqlalchemy.orm.properties import ColumnProperty from sqlalchemy.orm.properties import ColumnProperty
@@ -1397,14 +1397,14 @@ class PydRun(PydBaseClass): #, extra='allow'):
Converts this instance into a backend.db.models.procedure.BasicRun instance Converts this instance into a backend.db.models.procedure.BasicRun instance
Returns: Returns:
Tuple[BasicRun, Result]: BasicRun instance, result object Tuple[BasicRun, Alert]: BasicRun instance, result object
""" """
report = Report() report = Report()
dicto = self.improved_dict() dicto = self.improved_dict()
instance, result = Run.query_or_create(submissiontype=self.submission_type['value'], instance, result = Run.query_or_create(submissiontype=self.submission_type['value'],
rsl_plate_number=self.rsl_plate_number['value']) rsl_plate_number=self.rsl_plate_number['value'])
if instance is None: if instance is None:
report.add_result(Result(msg="Overwrite Cancelled.")) report.add_result(Alert(msg="Overwrite Cancelled."))
return None, report return None, report
report.add_result(result) report.add_result(result)
self.handle_duplicate_samples() self.handle_duplicate_samples()
@@ -1585,7 +1585,7 @@ class PydRun(PydBaseClass): #, extra='allow'):
expired.append(f"{reagent.role}, {reagent.lot}: {reagent.expiry.date()} + {role_eol.days}") expired.append(f"{reagent.role}, {reagent.lot}: {reagent.expiry.date()} + {role_eol.days}")
if expired: if expired:
output = '\n'.join(expired) output = '\n'.join(expired)
result = Result(status="Warning", result = Alert(status="Warning",
msg=f"The following reagents are expired:\n\n{output}" msg=f"The following reagents are expired:\n\n{output}"
) )
report.add_result(result) report.add_result(result)

View File

@@ -5,7 +5,7 @@ from datetime import date
from PyQt6.QtCore import QSignalBlocker from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QWidget, QGridLayout from PyQt6.QtWidgets import QWidget, QGridLayout
from tools import Report, report_result, Result from tools import Report, report_result, Alert
from .misc import StartEndDatePicker from .misc import StartEndDatePicker
from .functions import select_save_file, save_pdf from .functions import select_save_file, save_pdf
import logging import logging
@@ -42,7 +42,7 @@ class InfoPane(QWidget):
with QSignalBlocker(self.datepicker.start_date) as blocker: with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(lastmonth) self.datepicker.start_date.setDate(lastmonth)
self.update_data() self.update_data()
report.add_result(Result(owner=self.__str__(), msg=msg, status="Warning")) report.add_result(Alert(owner=self.__str__(), msg=msg, status="Warning"))
return report return report
@classmethod @classmethod

View File

@@ -109,9 +109,7 @@ class SubmissionsTree(QTreeView):
sets data in model sets data in model
""" """
self.clear() self.clear()
self.data = [item.to_dict(full_data=True) for item in self.data = [item.to_dict(full_data=True) for item in ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
# self.data = [item.details_dict() for item in
ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
root = self.model.invisibleRootItem() root = self.model.invisibleRootItem()
for submission in self.data: for submission in self.data:
group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}" group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}"

View File

@@ -9,7 +9,7 @@ from PyQt6.QtWidgets import (
from PyQt6.QtCore import pyqtSignal, Qt, QSignalBlocker from PyQt6.QtCore import pyqtSignal, Qt, QSignalBlocker
from .functions import select_open_file, select_save_file from .functions import select_open_file, select_save_file
from pathlib import Path from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent from tools import Report, Alert, check_not_nan, main_form_style, report_result, get_application_from_parent
from backend.validators import PydReagent, PydClientSubmission, PydSample from backend.validators import PydReagent, PydClientSubmission, PydSample
from backend.db.models import ( from backend.db.models import (
ClientLab, SubmissionType, Reagent, ReagentLot, ClientLab, SubmissionType, Reagent, ReagentLot,
@@ -116,7 +116,7 @@ class SubmissionFormContainer(QWidget):
if isinstance(fname, bool) or fname is None: if isinstance(fname, bool) or fname is None:
fname = select_open_file(self, file_extension="xlsx") fname = select_open_file(self, file_extension="xlsx")
if not fname: if not fname:
report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical")) report.add_result(Alert(msg=f"File {fname.__str__()} not found.", status="critical"))
return report return report
# NOTE: create sheetparser using excel sheet and context from gui # NOTE: create sheetparser using excel sheet and context from gui
self.clientsubmission_manager = DefaultClientSubmissionManager(parent=self, input_object=fname) self.clientsubmission_manager = DefaultClientSubmissionManager(parent=self, input_object=fname)
@@ -133,7 +133,7 @@ class SubmissionFormContainer(QWidget):
else: else:
message = "Submission cancelled." message = "Submission cancelled."
logger.warning(message) logger.warning(message)
report.add_result(Result(msg=message, owner=self.__class__.__name__, status="Warning")) report.add_result(Alert(msg=message, owner=self.__class__.__name__, status="Warning"))
return report return report
@report_result @report_result
@@ -157,7 +157,7 @@ class SubmissionFormContainer(QWidget):
# NOTE: send reagent to db # NOTE: send reagent to db
sqlobj = reagent.to_sql() sqlobj = reagent.to_sql()
sqlobj.save() sqlobj.save()
report.add_result(Result(owner=__name__, code=0, msg="New reagent created.", status="Information")) report.add_result(Alert(owner=__name__, code=0, msg="New reagent created.", status="Information"))
return reagent, report return reagent, report
@@ -386,7 +386,7 @@ class SubmissionFormWidget(QWidget):
if reagent is not None: if reagent is not None:
reagents.append(reagent) reagents.append(reagent)
else: else:
report.add_result(Result(msg="Failed integrity check", status="Critical")) report.add_result(Alert(msg="Failed integrity check", status="Critical"))
return report return report
case self.InfoItem(): case self.InfoItem():
field, value = widget.parse_form() field, value = widget.parse_form()
@@ -779,7 +779,7 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
if reagent is not None: if reagent is not None:
reagents.append(reagent) reagents.append(reagent)
else: else:
report.add_result(Result(msg="Failed integrity check", status="Critical")) report.add_result(Alert(msg="Failed integrity check", status="Critical"))
return report return report
case self.InfoItem(): case self.InfoItem():
field, value = widget.parse_form() field, value = widget.parse_form()

View File

@@ -104,8 +104,6 @@ div.gallery {
padding: 5px; padding: 5px;
} }
.plate { .plate {
display: inline-grid; display: inline-grid;
grid-auto-flow: column; grid-auto-flow: column;
@@ -189,3 +187,9 @@ ul.no-bullets {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
} }
.disable_section {
pointer-events: none;
opacity: 0.4;
}

View File

@@ -25,7 +25,7 @@
{% block script %} {% block script %}
{% if not child %} {% if not child %}
{% for j in js%} {% for j in js %}
<script> <script>
{{ j }} {{ j }}

View File

@@ -458,7 +458,6 @@ def render_details_template(template_name: str, css_in: List[str] | str = [], js
js_in = ["details"] + js_in js_in = ["details"] + js_in
js_in = [html_folder.joinpath("js", f"{j}.js") for j in js_in] js_in = [html_folder.joinpath("js", f"{j}.js") for j in js_in]
template = env.get_template(f"{template_name}.html") template = env.get_template(f"{template_name}.html")
# template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
css_out = [] css_out = []
for css in css_in: for css in css_in:
with open(css, "r") as f: with open(css, "r") as f:
@@ -645,7 +644,7 @@ def get_application_from_parent(widget):
return widget return widget
class Result(BaseModel, arbitrary_types_allowed=True): class Alert(BaseModel, arbitrary_types_allowed=True):
owner: str = Field(default="", validate_default=True) owner: str = Field(default="", validate_default=True)
code: int = Field(default=0) code: int = Field(default=0)
msg: str | Exception msg: str | Exception
@@ -704,7 +703,7 @@ class Result(BaseModel, arbitrary_types_allowed=True):
class Report(BaseModel): class Report(BaseModel):
results: List[Result] = Field(default=[]) results: List[Alert] = Field(default=[])
def __repr__(self): def __repr__(self):
return f"<Report(result_count:{len(self.results)})>" return f"<Report(result_count:{len(self.results)})>"
@@ -717,10 +716,10 @@ class Report(BaseModel):
Takes a result object or all results in another report and adds them to this one. Takes a result object or all results in another report and adds them to this one.
Args: Args:
result (Result | Report | None): Results to be added. result (Alert | Report | None): Results to be added.
""" """
match result: match result:
case Result(): case Alert():
logger.info(f"Adding {result} to results.") logger.info(f"Adding {result} to results.")
try: try:
self.results.append(result) self.results.append(result)
@@ -853,7 +852,7 @@ def check_authorization(func):
logger.error(error_msg) logger.error(error_msg)
report = Report() report = Report()
report.add_result( report.add_result(
Result(owner=func.__str__(), code=1, msg=error_msg, status="warning")) Alert(owner=func.__str__(), code=1, msg=error_msg, status="warning"))
return report, kwargs return report, kwargs
return wrapper return wrapper
@@ -877,7 +876,7 @@ def under_development(func):
logger.error(error_msg) logger.error(error_msg)
report = Report() report = Report()
report.add_result( report.add_result(
Result(owner=func.__str__(), code=1, msg=error_msg, Alert(owner=func.__str__(), code=1, msg=error_msg,
status="warning")) status="warning"))
return report return report
return wrapper return wrapper