Source code for bhf_smarthealth.process.summary_process

"""This module utilises functions from the parse.summary_parser module to batch process raw Fitbit daily summary data"""

import logging
import pandas as pd

from pathlib import Path

from bhf_smarthealth.log import metadata_logs
from bhf_smarthealth.parse import summary_parser as bhf_sum
from bhf_smarthealth.utils import df_utils, data_io
from bhf_smarthealth.exceptions import (
    RawDataProcessingError,
    EmptyFileError,
)
from bhf_smarthealth.models import DataStreamNames

logger = logging.getLogger(__name__)


[docs] def process_file(part_path: Path) -> pd.DataFrame: # returns None if no file matches data_path = next(part_path.glob("Fitbit/*_fitbit.json"), None) if not data_path: raise FileNotFoundError( f"No file matching the glob pattern 'Fitbit/*_fitbit.json' could be found in the directory {part_path}" ) logger.info(f"Processing {data_path}") data_dict = bhf_sum.load_json(data_path) parsed_data = bhf_sum.validate_data(data_dict) data_df = bhf_sum.create_dataframe(parsed_data) logger.info( f"Processed {part_path}. Final dataframe shape: {data_df.shape}. Dataframe columns: {data_df.columns} with index: {data_df.index.names}" ) return data_df
[docs] def process_day(in_dir: Path, out_paths: dict[str, Path], date_str: str): """Process a daily batch of raw Fitbit daily summary .json files Parameters ---------- in_dir : Path Path of the directory containing raw data to batch process out_path : Path Path to output processed data files to """ df_list = [] quality_counter = metadata_logs.DataQualityCounter() part_paths = [ part_path for part_path in sorted(in_dir.iterdir()) if part_path.is_dir() ] for part_path in part_paths: try: processed_df = process_file(part_path) except EmptyFileError: logger.error(f"File {part_path} is empty. Skipping file..") quality_counter["empty"] += 1 continue except RawDataProcessingError: logger.exception( f"Error with raw Health App data file at {part_path}. Skipping file.." ) quality_counter["processing_error"] += 1 continue except FileNotFoundError: logger.error(f"File missing in {part_path}. Skipping file..") quality_counter["missing"] += 1 continue else: df_list.append(processed_df) quality_counter["valid"] += 1 # check whether enough valid data was processed metadata_logs.evaluate_daily_processing_success(counter=quality_counter) # back stop for DEFAULT_VALID_DATA_THRESHOLD being set to 0 if quality_counter["valid"] == 0: full_df = pd.DataFrame() valid_IDs = [] logger.warning("Writing empty file..") else: full_df = df_utils.merge_dataframes(df_list) full_df = full_df.set_index(["sheffield_id", "date"]) valid_IDs = full_df.reset_index()["sheffield_id"].unique().tolist() data_io.save_dataframe_ouput(full_df, out_paths["interim"]) if not full_df.empty: logger.info( f"Merged DataFrame saved to {out_paths['interim']} with columns: {full_df.columns}" ) # output metadata log of all IDs seen 'today' all_IDs = [str(part_path.stem) for part_path in part_paths] # Both summary and intraday stream receipts will have "fitbit" as the source stream, but will be saved out to different path names to be consolidated at a later stage. metadata_logs.generate_daily_IDs_receipt( all_IDs=all_IDs, valid_IDs=valid_IDs, data_stream=DataStreamNames.FITBIT, date_str=date_str, out_path=out_paths["metadata"], )