Skip to content

Loader

apple_health_parser.utils.loader.Loader

Loader class to extract and read an XML file from the Apple Health export.zip file.

Source code in apple_health_parser/utils/loader.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
class Loader:
    """
    Loader class to extract and read an XML file from the Apple Health `export.zip` file.
    """

    @staticmethod
    def extract_zip(
        zip_file: str | Path, output_dir: str | Path, overwrite: bool | None = None
    ) -> Path:
        """
        Extracts a zip file to an output directory.

        Args:
            zip_file (str | Path): The zip file to extract
            output_dir (str | Path): The output directory to extract the file to
            overwrite (bool, optional): Flag to overwrite the existing data, defaults to None

        Returns:
            Path: The absolute path to the extracted file
        """
        if isinstance(zip_file, str):
            zip_file = Path(zip_file)

        if isinstance(output_dir, str):
            output_dir = Path(output_dir)

        export_dir = output_dir / "apple_health_export"

        # Check if output directory exists and delete it if it does and "y" or "yes" is entered
        if export_dir.exists():
            Loader.delete_previous_export(export_dir, overwrite)

        # Extract the zip file
        with ZipFile(zip_file, "r") as data:
            logger.info(f"Extracting {zip_file} to {output_dir}...")
            data.extractall(output_dir)

        # Log the compressed and uncompressed file sizes
        file_size = zip_file.stat().st_size
        dir_size = sum(f.stat().st_size for f in output_dir.glob("**/*") if f.is_file())
        logger.info(f"Compressed: {file_size / 1e6:.2f} MB")
        logger.info(f"Uncompressed: {dir_size/ 1e6:.2f} MB")

        return (export_dir / "export.xml").resolve()

    @staticmethod
    def delete_previous_export(output_dir: Path, overwrite: bool | None) -> None:
        """
        Delete the previous export if it exists and the user agrees.

        Args:
            output_dir (Path): The output directory to extract the file to
            overwrite (bool | None): Flag to overwrite the existing data, defaults to None
        """
        match overwrite:
            case None:
                logger.warning(f"Found previous export at {output_dir}...")
                if click.confirm("Do you want to delete it?"):
                    rmtree(output_dir)
                    logger.warning(f"Deleted previous export at {output_dir}...")

            case True:
                rmtree(output_dir)
                logger.warning(f"Deleted previous export at {output_dir}...")

    @staticmethod
    def read_xml(xml_file: Path) -> list[ET.Element]:
        """
        Read an XML file and return the root element.

        Args:
            xml_file (Path): Path to the XML file

        Returns:
            list[ET.Element]: List of records (ET.Element)
        """
        logger.info(f"Processing {xml_file}...")
        with open(xml_file, "r") as file:
            root = ET.parse(file).getroot()
            Loader._log_metadata(root)
            return root.findall("Record")

    @staticmethod
    def _log_metadata(root: ET.Element) -> None:
        """
        Log metadata from the XML file.

        Example:

        ```bash
        2024-05-29 22:28:23,064 - INFO - Locale:                            en_NL
        2024-05-29 22:28:23,065 - INFO - Export date:                       2024-05-30 22:20:25 +0200
        2024-05-29 22:28:23,065 - INFO - Date of birth:                     1990-04-22
        2024-05-29 22:28:23,065 - INFO - Biological sex:                    HKBiologicalSexMale
        2024-05-29 22:28:23,065 - INFO - Blood type:                        HKBloodTypeAPositive
        2024-05-29 22:28:23,065 - INFO - Fitzpatrick skin type:             HKFitzpatrickSkinTypeNotSet
        2024-05-29 22:28:23,065 - INFO - Cardio fitness medications use:    None
        ```

        Args:
            root (ET.Element): Root element of the XML file
        """

        def get_locale() -> None:
            """
            Get the locale from the XML file (e.g. `en_NL`).
            """
            locale = root.attrib.get("locale")
            if locale is not None:
                logger.info(f"{'Locale:':<35}" + click.style(f"{locale}", bg="red"))

        def get_export_date() -> None:
            """
            Get the export date from the XML file (e.g. `2024-05-29 22:20:35 +0200`).
            """
            export_date = root.find("ExportDate")
            if export_date is not None:
                logger.info(
                    f"{'Export date:':<35}"
                    + click.style(f"{export_date.attrib['value']}", fg="green")
                )

        def get_user() -> None:
            """
            Get user metadata from the XML file.
            """
            user = root.find("Me")
            if user is not None:
                user_data: list[tuple[str, str]] = [
                    (flag.removeprefix("HKCharacteristicTypeIdentifier"), value)
                    for (flag, value) in user.items()
                ]
                for flag, value in user_data:
                    # Split flag at uppercase letters
                    flag = " ".join(re.findall(r"[A-Z][^A-Z]*", flag))
                    # Lower case every word except the first
                    flag = flag[0] + flag[1:].lower() + ":"
                    logger.info(f"{flag:<35}" + click.style(f"{value}", fg="cyan"))

        get_locale()
        get_export_date()
        get_user()

delete_previous_export(output_dir, overwrite) staticmethod

Delete the previous export if it exists and the user agrees.

Parameters:

Name Type Description Default
output_dir Path

The output directory to extract the file to

required
overwrite bool | None

Flag to overwrite the existing data, defaults to None

required
Source code in apple_health_parser/utils/loader.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@staticmethod
def delete_previous_export(output_dir: Path, overwrite: bool | None) -> None:
    """
    Delete the previous export if it exists and the user agrees.

    Args:
        output_dir (Path): The output directory to extract the file to
        overwrite (bool | None): Flag to overwrite the existing data, defaults to None
    """
    match overwrite:
        case None:
            logger.warning(f"Found previous export at {output_dir}...")
            if click.confirm("Do you want to delete it?"):
                rmtree(output_dir)
                logger.warning(f"Deleted previous export at {output_dir}...")

        case True:
            rmtree(output_dir)
            logger.warning(f"Deleted previous export at {output_dir}...")

extract_zip(zip_file, output_dir, overwrite=None) staticmethod

Extracts a zip file to an output directory.

Parameters:

Name Type Description Default
zip_file str | Path

The zip file to extract

required
output_dir str | Path

The output directory to extract the file to

required
overwrite bool

Flag to overwrite the existing data, defaults to None

None

Returns:

Name Type Description
Path Path

The absolute path to the extracted file

Source code in apple_health_parser/utils/loader.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@staticmethod
def extract_zip(
    zip_file: str | Path, output_dir: str | Path, overwrite: bool | None = None
) -> Path:
    """
    Extracts a zip file to an output directory.

    Args:
        zip_file (str | Path): The zip file to extract
        output_dir (str | Path): The output directory to extract the file to
        overwrite (bool, optional): Flag to overwrite the existing data, defaults to None

    Returns:
        Path: The absolute path to the extracted file
    """
    if isinstance(zip_file, str):
        zip_file = Path(zip_file)

    if isinstance(output_dir, str):
        output_dir = Path(output_dir)

    export_dir = output_dir / "apple_health_export"

    # Check if output directory exists and delete it if it does and "y" or "yes" is entered
    if export_dir.exists():
        Loader.delete_previous_export(export_dir, overwrite)

    # Extract the zip file
    with ZipFile(zip_file, "r") as data:
        logger.info(f"Extracting {zip_file} to {output_dir}...")
        data.extractall(output_dir)

    # Log the compressed and uncompressed file sizes
    file_size = zip_file.stat().st_size
    dir_size = sum(f.stat().st_size for f in output_dir.glob("**/*") if f.is_file())
    logger.info(f"Compressed: {file_size / 1e6:.2f} MB")
    logger.info(f"Uncompressed: {dir_size/ 1e6:.2f} MB")

    return (export_dir / "export.xml").resolve()

read_xml(xml_file) staticmethod

Read an XML file and return the root element.

Parameters:

Name Type Description Default
xml_file Path

Path to the XML file

required

Returns:

Type Description
list[Element]

list[ET.Element]: List of records (ET.Element)

Source code in apple_health_parser/utils/loader.py
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@staticmethod
def read_xml(xml_file: Path) -> list[ET.Element]:
    """
    Read an XML file and return the root element.

    Args:
        xml_file (Path): Path to the XML file

    Returns:
        list[ET.Element]: List of records (ET.Element)
    """
    logger.info(f"Processing {xml_file}...")
    with open(xml_file, "r") as file:
        root = ET.parse(file).getroot()
        Loader._log_metadata(root)
        return root.findall("Record")