import os
import csv
import logging
from dataclasses import dataclass, field
from typing import Any, Generator, Protocol, runtime_checkable
from datetime import datetime, timedelta
from pandas import DataFrame
from tabulate import tabulate
logger_main = logging.getLogger(__name__)
[docs]@runtime_checkable
class ReportProtocol(Protocol):
"""Protocol class for report object.
:param name: report name, propagated to report file name
:type name: str
:param id: report id
:type id: str
:param path: report path, save location for the report in form of :class:`pathlib.Path` object
:type path: :class:`os.PathLike`
:param type: report type, drives connector selection
:type type: str
:param export_params: default GET parameters
:type export_params: str
:param downloaded: flag indicating whether the reports has been succesfully downloaded or not
:type downloaded: bool
:param valid: flag indicating whether the response has been succesfully retrieved or not
:type valid: bool
:param created_date: report save completition date
:type created_date: datetime
:param pull_date: report response completition date
:type pull_date: datetime
:param processing_time: the time it took to process the report in seconds
:type pull_date: timedelta
:param attempt_count: number of attempts to process the report
:type attempt_count: int
:param size: size of saved report file in Mb
:type size: float
:param response: container for request response
:type response: str
:param content: Pandas :class:`pd.DataFrame` based on response
:type content: :class:`pd.DataFrame`
"""
name: str
id: str
path: os.PathLike
type: str
export_params: str
downloaded: bool
valid: bool
created_date: datetime
pull_date: datetime
processing_time: timedelta
attempt_count: int
size: float
response: str
content: DataFrame
[docs]@runtime_checkable
class ReportsContainerProtocol(Protocol):
"""Protocol class for report container object.
:param report_params_list: collection of dictionaries with parameters for object crafting
:type report_params_list: list[dict[str, Any]]
:param summary_report_path: path to save location of summary report
:type summary_report_path: :class:`os.PathLike`
"""
[docs] def create_reports(self) -> list[ReportProtocol]:
"""Orchestrating method to handle report objects factory
:return: collection of :class:`ReportProtocol` objects
:rtype: list[:class:`ReportProtocol`]
"""
...
[docs] def create_summary_report(self) -> None:
"""Creates summary report which consist of all important details regarding :class:`ReportProtocol` objects.
Summary report is generated once all the reports are completed.
"""
...
[docs]@dataclass(slots=True)
class SfdcReport():
"""Concrete class representing SFDC Report object.
:param name: report name, propagated to report file name
:type name: str
:param id: report id, identification number of the report in SFDC
:type id: str
:param path: report path, save location for the report in form of :class:`pathlib.Path` object
:type path: :class:`os.PathLike`
:param type: report type, type drives connector and report objects selectiond, defaults to 'SFDC'
:type type: str, optional
:param export_params: default GET parameters required by SFDC, defaults to '?export=csv&enc=UTF-8&isdtp=p1'
:type export_params: str, optional
:param downloaded: flag indicating whether the reports has been succesfully downloaded or not, defaults to False
:type downloaded: bool, optional
:param valid: flag indicating whether the response has been succesfully retrieved or not, defaults to False
:type valid: bool, optional
:param created_date: report save completition date, defaults to current datetime
:type created_date: datetime, optional
:param pull_date: report response completition date, defaults to current datetime
:type pull_date: datetime, optional
:param processing_time: the time it took to process the report in seconds, defaults to 0 microseconds
:type pull_date: timedelta, optional
:param attempt_count: number of attempts to process the report, defaults to 0
:type attempt_count: int, optional
:param size: size of saved report file in Mb, defaults to 0.0
:type size: float, optional
:param response: container for request response, defaults ""
:type response: str, optional
:param content: Pandas DataFrame based on response, defaults to empty :class:`pd.DataFrame`
:type content: :class:`pd.DataFrame` , optional
"""
name: str
id: str
path: os.PathLike
type: str = 'SFDC'
export_params: str = '?export=csv&enc=UTF-8&isdtp=p1'
downloaded: bool = False
valid: bool = False
created_date: datetime = datetime.now()
pull_date: datetime = datetime.now()
processing_time: timedelta = timedelta(microseconds=0)
attempt_count: int = 0
size: float = 0.0
response: str = ""
content: DataFrame = field(default_factory=DataFrame)
[docs]class ReportsContainer():
"""Concrete class representing ReportContainer object.
"""
def __init__(self,
reports_params_list: list[dict[str, Any]],
summary_path: os.PathLike | None):
"""Constructor method for ReportContainer, automatically creates reports after initialization
"""
self.reports_params_list: list[dict[str, Any]] = reports_params_list
self.summary_path: os.PathLike | None = summary_path
self.reports_list: list[ReportProtocol]
self.create_reports()
def __len__(self):
"""Returns number of collected :class:`ReportProtocol` objects.
"""
return len(self.reports_list)
[docs] def _create_sfdc_reports(self) -> Generator[SfdcReport, None, None]:
"""Creates :class:`SfdcReport` objects.
:return: generator with :class:`SfdcReport` objects
:rtype: Generator[:class:`SfdcReport`, None, None]
:yield: :class:`SfdcReport` instance based on parsed report parameters
:rtype: :class:`SfdcReport`
"""
logger_main.debug("Creating SFDC report objects")
reports = (SfdcReport(**dict) for dict in self.reports_params_list)
return reports
[docs] def create_reports(self) -> list[ReportProtocol]:
"""Orchestrating method to handle report objects crafting
:return: collection of :class:`ReportProtocol` objects
:rtype: list[:class:`ReportProtocol`]
"""
logger_main.debug("Creating all report objects")
self.reports_list = list(self._create_sfdc_reports())
return self.reports_list
[docs] def _create_summary_folder_if_not_exist(self):
"""Creates folder for summary report if doesn't exist.
"""
if self.summary_path is not None:
if not os.path.exists(os.path.dirname(self.summary_path)):
os.makedirs(os.path.dirname(self.summary_path))
return None
[docs] def create_summary_report(self) -> None:
"""Creates summary report which consist of all important details regarding :class:`ReportProtocol` objects.
Summary is generated once all the :class:`ReportProtocol` are completed.
"""
if self.summary_path:
logger_main.debug("Creating summary report, saved in %s",
self.summary_path)
self._create_summary_folder_if_not_exist()
header = ['file_name', 'report_id', 'type', 'valid', 'created_date',
'pull_date', 'processing_time', 'attempt_count', 'file_size']
with open(self.summary_path, 'w', encoding='UTF8', newline='') as f:
writer = csv.writer(f)
writer.writerow(header)
for report in self.reports_list:
writer.writerow([report.name, report.id, report.type, report.valid, report.created_date,
report.pull_date, report.processing_time, report.attempt_count, report.size])
return None
[docs] def print_summary_table(self) -> None:
"""Prints summary table which consist of all important details regarding :class:`ReportProtocol` objects.
Table is generated once all the :class:`ReportProtocol` are completed.
"""
logger_main.debug("Creating summary table")
print("")
header = ['report', 'valid', 'processing_time', 'file_size']
data = []
for report in self.reports_list:
data.append([report.name, report.valid, report.processing_time, report.size])
print(tabulate(data, headers=header, tablefmt='fancy_grid'))
return None
if __name__ == '__main__':
pass