Source code for bhf_smarthealth.parse.summary_parser

"""This module contains functions to process raw Fitbit daily summary data ingressed to the Smart Health pipeline"""

import pandas as pd
import logging
from typing import Dict, Any
from pydantic import ValidationError
from pathlib import Path
from bhf_smarthealth.validate.fitbit_schema import FitbitSummaryData
from bhf_smarthealth.utils import data_io
from bhf_smarthealth.exceptions import DataValidationError, EmptyFileError
from bhf_smarthealth.validate import metadata_validation

logger = logging.getLogger(__name__)


[docs] def load_json(json_path: str | Path) -> Dict[str, Any]: """ Load the raw JSON file, reshape it into a flat dictionary, and validate data consistency against filename metadata. The function extracts the Sheffield ID and date from the filename and compares the filename date against all date portions of timestamps found within the data for consistency. Files with inconsistent dates (either externally or internally inconsistent) will raise a ValueError. Parameters ---------- json_path : str Path to the data file to be processed. Returns ------- Dict[str, Any] A flat dictionary containing the raw, unvalidated Fitbit daily summary data, with sheffield_id and date appended Raises ------ ValueError - If the filename date is known but data dates do not match it (External Consistency Failure). - If the filename date is unknown (Mandatory Error). """ # Load raw JSON data json_dict = data_io.load_data_from_file(json_path) if not json_dict: raise EmptyFileError(f"No data to process in file {json_path}") # Convert original, nested json into something flatter data_dict = {key: value[0] for key, value in json_dict.items() if value} # Extract a list of dates from the datapoints dates = [ value[0].get("dateTime") for value in json_dict.values() if isinstance(value, list) and len(value) > 0 and "dateTime" in value[0] ] # Extract and validate the metadata in the filename metadata = metadata_validation.process_metadata(filepath=json_path, dates=dates) # Add the validated metadata to the data dict data_dict = { "date": metadata.file_date, "sheffield_id": metadata.sheffield_id, **data_dict, } logger.info(f"Loaded Daily Summary data from {json_path}") logger.debug(f"Loaded Daily Summary data contains columns: {data_dict.keys()}") return data_dict
[docs] def validate_data(data_dict: Dict[str, Any]) -> FitbitSummaryData: """Uses Pydantic model to convert and validate the dictionary containig the raw data. This should raise an error if there is an issue. Parameters ---------- data_dict : dict[str, str] Dictionary of the raw data Returns ------- FitbitSummaryData Pydantic model class containing the converted data Raises ------ DataValidationError Raised if the pydantic schema validation fails """ try: return FitbitSummaryData(**data_dict) except (ValidationError, ValueError) as e: # raised ValidationError is re-raised as a custom error that inherits from the RawDataProcessingError exception class raise DataValidationError(f"Schema validation failed: {e}") from e
[docs] def create_dataframe(summary_data: FitbitSummaryData) -> pd.DataFrame: """Creates a dataframe from the Pydantic model with nested FitbitSummaryDatapoints under the daily activity fields. From these nested fields, the "value" is extracted. Parameters ---------- summary_data : FitbitSummaryData Pydantic model class containing the converted data Returns ------- pd.DataFrame DataFrame containing data """ validated_dict = summary_data.model_dump() # Need to extract value from the nested metric fields extracted_dict = { key: (val.get("value") if isinstance(val, dict) else val) for key, val in validated_dict.items() } df = pd.DataFrame([extracted_dict]) logger.debug(f"DataFrame created with shape {df.shape}") return df