Refined query-by-date to use start/end of day times to improve accuracy.
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
# 202504.02
|
||||||
|
|
||||||
|
- Refined query-by-date to use start/end of day times to improve accuracy.
|
||||||
|
|
||||||
# 202504.01
|
# 202504.01
|
||||||
|
|
||||||
- Added in method to backup submissions to xlsx (partly).
|
- Added in method to backup submissions to xlsx (partly).
|
||||||
|
|||||||
3
TODO.md
3
TODO.md
@@ -1,3 +1,6 @@
|
|||||||
|
- [x] Apply below fix to all other date-based queries.
|
||||||
|
- [x] Fix Control graph chart bug that excludes today's controls.
|
||||||
|
- [x] Convert logger to a custom class.
|
||||||
- [x] Change "Manage Organizations" to the Pydantic version.
|
- [x] Change "Manage Organizations" to the Pydantic version.
|
||||||
- [x] Can my "to_dict", "to_sub_dict", "to_pydantic" methods be rewritten as properties?
|
- [x] Can my "to_dict", "to_sub_dict", "to_pydantic" methods be rewritten as properties?
|
||||||
- [ ] Stop displacing date on Irida controls and just do what Turnaround time does.
|
- [ ] Stop displacing date on Irida controls and just do what Turnaround time does.
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
import logging
|
||||||
import sys, os
|
import sys, os
|
||||||
from tools import ctx, setup_logger, check_if_app
|
from tools import ctx, check_if_app, CustomLogger
|
||||||
|
|
||||||
# NOTE: environment variable must be set to enable qtwebengine in network path
|
# NOTE: environment variable must be set to enable qtwebengine in network path
|
||||||
if check_if_app():
|
if check_if_app():
|
||||||
os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1"
|
os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1"
|
||||||
|
|
||||||
# NOTE: setup custom logger
|
# NOTE: setup custom logger
|
||||||
logger = setup_logger(verbosity=3)
|
logging.setLoggerClass(CustomLogger)
|
||||||
|
# logger = logging.getLogger("submissions")
|
||||||
|
# logger = setup_logger(verbosity=3)
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
from frontend.widgets.app import App
|
from frontend.widgets.app import App
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ from sqlalchemy.orm import relationship, Query, validates
|
|||||||
import logging, re
|
import logging, re
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from . import BaseClass
|
from . import BaseClass
|
||||||
from tools import setup_lookup, report_result, Result, Report, Settings, get_unique_values_in_df_column, super_splitter
|
from tools import setup_lookup, report_result, Result, Report, Settings, get_unique_values_in_df_column, super_splitter, \
|
||||||
|
rectify_query_date
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import List, Literal, Tuple, Generator
|
from typing import List, Literal, Tuple, Generator
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
@@ -149,8 +150,8 @@ class Control(BaseClass):
|
|||||||
def query(cls,
|
def query(cls,
|
||||||
submissiontype: str | None = None,
|
submissiontype: str | None = None,
|
||||||
subtype: str | None = None,
|
subtype: str | None = None,
|
||||||
start_date: date | str | int | None = None,
|
start_date: date | datetime | str | int | None = None,
|
||||||
end_date: date | str | int | None = None,
|
end_date: date | datetime | str | int | None = None,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
limit: int = 0, **kwargs
|
limit: int = 0, **kwargs
|
||||||
) -> Control | List[Control]:
|
) -> Control | List[Control]:
|
||||||
@@ -201,22 +202,30 @@ class Control(BaseClass):
|
|||||||
logger.warning(f"End date with no start date, using 90 days ago.")
|
logger.warning(f"End date with no start date, using 90 days ago.")
|
||||||
start_date = date.today() - timedelta(days=90)
|
start_date = date.today() - timedelta(days=90)
|
||||||
if start_date is not None:
|
if start_date is not None:
|
||||||
match start_date:
|
# match start_date:
|
||||||
case date():
|
# case datetime():
|
||||||
start_date = start_date.strftime("%Y-%m-%d")
|
# start_date = start_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
case int():
|
# case date():
|
||||||
start_date = datetime.fromordinal(
|
# start_date = datetime.combine(start_date, datetime.min.time())
|
||||||
datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
|
# start_date = start_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
case _:
|
# case int():
|
||||||
start_date = parse(start_date).strftime("%Y-%m-%d")
|
# start_date = datetime.fromordinal(
|
||||||
match end_date:
|
# datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
case date():
|
# case _:
|
||||||
end_date = end_date.strftime("%Y-%m-%d")
|
# start_date = parse(start_date).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
case int():
|
start_date = rectify_query_date(start_date)
|
||||||
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
|
end_date = rectify_query_date(end_date, eod=True)
|
||||||
"%Y-%m-%d")
|
# match end_date:
|
||||||
case _:
|
# case datetime():
|
||||||
end_date = parse(end_date).strftime("%Y-%m-%d")
|
# end_date = end_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
# case date():
|
||||||
|
# end_date = datetime.combine(end_date, datetime.max.time())
|
||||||
|
# end_date = end_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
# case int():
|
||||||
|
# end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
|
||||||
|
# "%Y-%m-%d %H:%M:%S")
|
||||||
|
# case _:
|
||||||
|
# end_date = parse(end_date).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
query = query.filter(cls.submitted_date.between(start_date, end_date))
|
query = query.filter(cls.submitted_date.between(start_date, end_date))
|
||||||
match name:
|
match name:
|
||||||
case str():
|
case str():
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as S
|
|||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
from openpyxl.drawing.image import Image as OpenpyxlImage
|
from openpyxl.drawing.image import Image as OpenpyxlImage
|
||||||
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \
|
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \
|
||||||
report_result, create_holidays_for_year, check_dictionary_inclusion_equality
|
report_result, create_holidays_for_year, check_dictionary_inclusion_equality, rectify_query_date
|
||||||
from datetime import datetime, date, timedelta
|
from datetime import datetime, date, timedelta
|
||||||
from typing import List, Any, Tuple, Literal, Generator, Type
|
from typing import List, Any, Tuple, Literal, Generator, Type
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
@@ -1152,35 +1152,37 @@ class BasicSubmission(BaseClass, LogMixin):
|
|||||||
start_date = cls.__database_session__.query(cls, func.min(cls.submitted_date)).first()[1]
|
start_date = cls.__database_session__.query(cls, func.min(cls.submitted_date)).first()[1]
|
||||||
logger.warning(f"End date with no start date, using first submission date: {start_date}")
|
logger.warning(f"End date with no start date, using first submission date: {start_date}")
|
||||||
if start_date is not None:
|
if start_date is not None:
|
||||||
match start_date:
|
# match start_date:
|
||||||
case date():
|
# case date():
|
||||||
pass
|
# pass
|
||||||
case datetime():
|
# case datetime():
|
||||||
start_date = start_date.date()
|
# start_date = start_date.date()
|
||||||
case int():
|
# case int():
|
||||||
start_date = datetime.fromordinal(
|
# start_date = datetime.fromordinal(
|
||||||
datetime(1900, 1, 1).toordinal() + start_date - 2).date()
|
# datetime(1900, 1, 1).toordinal() + start_date - 2).date()
|
||||||
case _:
|
# case _:
|
||||||
start_date = parse(start_date).date()
|
# start_date = parse(start_date).date()
|
||||||
# start_date = start_date.strftime("%Y-%m-%d")
|
# # start_date = start_date.strftime("%Y-%m-%d")
|
||||||
match end_date:
|
# match end_date:
|
||||||
case date():
|
# case date():
|
||||||
pass
|
# pass
|
||||||
case datetime():
|
# case datetime():
|
||||||
end_date = end_date # + timedelta(days=1)
|
# end_date = end_date # + timedelta(days=1)
|
||||||
# pass
|
# # pass
|
||||||
case int():
|
# case int():
|
||||||
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date() # \
|
# end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date() # \
|
||||||
# + timedelta(days=1)
|
# # + timedelta(days=1)
|
||||||
case _:
|
# case _:
|
||||||
end_date = parse(end_date).date() # + timedelta(days=1)
|
# end_date = parse(end_date).date() # + timedelta(days=1)
|
||||||
# end_date = end_date.strftime("%Y-%m-%d")
|
# # end_date = end_date.strftime("%Y-%m-%d")
|
||||||
start_date = datetime.combine(start_date, datetime.min.time()).strftime("%Y-%m-%d %H:%M:%S.%f")
|
# start_date = datetime.combine(start_date, datetime.min.time()).strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
end_date = datetime.combine(end_date, datetime.max.time()).strftime("%Y-%m-%d %H:%M:%S.%f")
|
# end_date = datetime.combine(end_date, datetime.max.time()).strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
# if start_date == end_date:
|
# # if start_date == end_date:
|
||||||
# start_date = start_date.strftime("%Y-%m-%d %H:%M:%S.%f")
|
# # start_date = start_date.strftime("%Y-%m-%d %H:%M:%S.%f")
|
||||||
# query = query.filter(model.submitted_date == start_date)
|
# # query = query.filter(model.submitted_date == start_date)
|
||||||
# else:
|
# # else:
|
||||||
|
start_date = rectify_query_date(start_date)
|
||||||
|
end_date = rectify_query_date(end_date, eod=True)
|
||||||
query = query.filter(model.submitted_date.between(start_date, end_date))
|
query = query.filter(model.submitted_date.between(start_date, end_date))
|
||||||
# NOTE: by reagent (for some reason)
|
# NOTE: by reagent (for some reason)
|
||||||
match reagent:
|
match reagent:
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ class ControlsViewer(InfoPane):
|
|||||||
parent=self,
|
parent=self,
|
||||||
months=months
|
months=months
|
||||||
)
|
)
|
||||||
|
logger.debug(f"Chart settings: {chart_settings}")
|
||||||
self.fig = self.archetype.instance_class.make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx)
|
self.fig = self.archetype.instance_class.make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx)
|
||||||
self.report_obj = ChartReportMaker(df=self.fig.df, sheet_name=self.archetype.name)
|
self.report_obj = ChartReportMaker(df=self.fig.df, sheet_name=self.archetype.name)
|
||||||
if issubclass(self.fig.__class__, CustomFigure):
|
if issubclass(self.fig.__class__, CustomFigure):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
A pane to show info e.g. cost reports and turnaround times.
|
A pane to show info e.g. cost reports and turnaround times.
|
||||||
"""
|
"""
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
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
|
||||||
@@ -32,8 +32,11 @@ class InfoPane(QWidget):
|
|||||||
@report_result
|
@report_result
|
||||||
def update_data(self, *args, **kwargs):
|
def update_data(self, *args, **kwargs):
|
||||||
report = Report()
|
report = Report()
|
||||||
|
# self.start_date = self.datepicker.start_date.date().toPyDate()
|
||||||
|
# self.end_date = self.datepicker.end_date.date().toPyDate()
|
||||||
self.start_date = self.datepicker.start_date.date().toPyDate()
|
self.start_date = self.datepicker.start_date.date().toPyDate()
|
||||||
self.end_date = self.datepicker.end_date.date().toPyDate()
|
self.end_date = self.datepicker.end_date.date().toPyDate()
|
||||||
|
logger.debug(f"Start date: {self.start_date}, End date: {self.end_date}")
|
||||||
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
|
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
|
||||||
lastmonth = self.datepicker.end_date.date().addDays(-31)
|
lastmonth = self.datepicker.end_date.date().addDays(-31)
|
||||||
msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}."
|
msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}."
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ from json import JSONDecodeError
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
from inspect import getmembers, isfunction, stack
|
from inspect import getmembers, isfunction, stack
|
||||||
from dateutil.easter import easter
|
from dateutil.easter import easter
|
||||||
|
from dateutil.parser import parse
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from logging import handlers
|
from logging import handlers, Logger
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sqlalchemy.orm import Session, InstrumentedAttribute
|
from sqlalchemy.orm import Session, InstrumentedAttribute
|
||||||
from sqlalchemy import create_engine, text, MetaData
|
from sqlalchemy import create_engine, text, MetaData
|
||||||
@@ -294,6 +295,7 @@ class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
|
|||||||
|
|
||||||
|
|
||||||
class CustomFormatter(logging.Formatter):
|
class CustomFormatter(logging.Formatter):
|
||||||
|
|
||||||
class bcolors:
|
class bcolors:
|
||||||
HEADER = '\033[95m'
|
HEADER = '\033[95m'
|
||||||
OKBLUE = '\033[94m'
|
OKBLUE = '\033[94m'
|
||||||
@@ -339,6 +341,42 @@ class StreamToLogger(object):
|
|||||||
self.logger.log(self.log_level, line.rstrip())
|
self.logger.log(self.log_level, line.rstrip())
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLogger(Logger):
|
||||||
|
|
||||||
|
def __init__(self, name: str = "submissions", level=logging.DEBUG):
|
||||||
|
super().__init__(name, level)
|
||||||
|
self.extra_info = None
|
||||||
|
ch = logging.StreamHandler(stream=sys.stdout)
|
||||||
|
ch.name = "Stream"
|
||||||
|
ch.setLevel(self.level)
|
||||||
|
# NOTE: create formatter and add it to the handlers
|
||||||
|
ch.setFormatter(CustomFormatter())
|
||||||
|
# NOTE: add the handlers to the logger
|
||||||
|
self.addHandler(ch)
|
||||||
|
sys.excepthook = self.handle_exception
|
||||||
|
|
||||||
|
def info(self, msg, *args, xtra=None, **kwargs):
|
||||||
|
extra_info = xtra if xtra is not None else self.extra_info
|
||||||
|
super().info(msg, *args, extra=extra_info, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def handle_exception(cls, exc_type, exc_value, exc_traceback):
|
||||||
|
"""
|
||||||
|
System won't halt after error, except KeyboardInterrupt
|
||||||
|
|
||||||
|
Args:
|
||||||
|
exc_value ():
|
||||||
|
exc_traceback ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
if issubclass(exc_type, KeyboardInterrupt):
|
||||||
|
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||||
|
return
|
||||||
|
logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
|
||||||
|
|
||||||
|
|
||||||
def setup_logger(verbosity: int = 3):
|
def setup_logger(verbosity: int = 3):
|
||||||
"""
|
"""
|
||||||
Set logger levels using settings.
|
Set logger levels using settings.
|
||||||
@@ -858,6 +896,35 @@ def check_dictionary_inclusion_equality(listo: List[dict] | dict, dicto: dict) -
|
|||||||
raise TypeError(f"Unsupported variable: {type(listo)}")
|
raise TypeError(f"Unsupported variable: {type(listo)}")
|
||||||
|
|
||||||
|
|
||||||
|
def rectify_query_date(input_date, eod: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Converts input into a datetime string for querying purposes
|
||||||
|
|
||||||
|
Args:
|
||||||
|
eod (bool, optional): Whether to use max time to indicate end of day.
|
||||||
|
input_date ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
datetime: properly formated datetime
|
||||||
|
"""
|
||||||
|
match input_date:
|
||||||
|
case datetime():
|
||||||
|
output_date = input_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
case date():
|
||||||
|
if eod:
|
||||||
|
addition_time = datetime.max.time()
|
||||||
|
else:
|
||||||
|
addition_time = datetime.min.time()
|
||||||
|
output_date = datetime.combine(input_date, addition_time)
|
||||||
|
output_date = output_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
case int():
|
||||||
|
output_date = datetime.fromordinal(
|
||||||
|
datetime(1900, 1, 1).toordinal() + input_date - 2).date().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
case _:
|
||||||
|
output_date = parse(input_date).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
return output_date
|
||||||
|
|
||||||
|
|
||||||
class classproperty(property):
|
class classproperty(property):
|
||||||
def __get__(self, owner_self, owner_cls):
|
def __get__(self, owner_self, owner_cls):
|
||||||
return self.fget(owner_cls)
|
return self.fget(owner_cls)
|
||||||
|
|||||||
Reference in New Issue
Block a user