├── tests └── __init__.py ├── health ├── constants │ ├── __init__.py │ ├── correlation.py │ ├── metadata.py │ ├── me.py │ ├── workout.py │ └── record.py ├── __init__.py ├── classes │ ├── __init__.py │ ├── correlation.py │ ├── record.py │ ├── me.py │ ├── base.py │ ├── workout.py │ └── activity_summary.py ├── util.py └── data.py ├── .gitignore ├── sonar-project.properties ├── .github └── workflows │ ├── publish.yml │ └── main.yml ├── pyproject.toml ├── LICENSE ├── README.md └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /health/constants/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /health/__init__.py: -------------------------------------------------------------------------------- 1 | from health.data import HealthData 2 | -------------------------------------------------------------------------------- /health/constants/correlation.py: -------------------------------------------------------------------------------- 1 | HK_CORRELATION_BLOOD_PRESSURE = "HKCorrelationTypeIdentifierBloodPressure" 2 | HK_CORRELATION_FOOD = "HKCorrelationTypeIdentifierFood" 3 | -------------------------------------------------------------------------------- /health/classes/__init__.py: -------------------------------------------------------------------------------- 1 | from health.classes.activity_summary import ActivitySummary 2 | from health.classes.correlation import Correlation 3 | from health.classes.me import Me 4 | from health.classes.record import Record 5 | from health.classes.workout import Workout 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # TMP 2 | tmp.py 3 | export/ 4 | 5 | # PyCharm 6 | .DS_Store 7 | .idea/ 8 | 9 | # Python 10 | *.py[co] 11 | 12 | # Packages 13 | *.egg 14 | *.egg-info 15 | dist 16 | build 17 | 18 | # Virtual Envs 19 | .venv/ 20 | venv/ 21 | 22 | # Testing 23 | coverage.xml 24 | .coverage 25 | coverage 26 | .scannerwork/ 27 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.organization=fedecalendino 2 | 3 | sonar.projectName=apple-health 4 | sonar.projectKey=fedecalendino_apple-health 5 | sonar.projectVersion=2.0.0 6 | 7 | sonar.language=py 8 | 9 | sonar.sources=health 10 | sonar.sourceEncoding=UTF-8 11 | 12 | sonar.python.coverage.reportPaths=coverage.xml 13 | -------------------------------------------------------------------------------- /health/constants/metadata.py: -------------------------------------------------------------------------------- 1 | HK_AVERAGE_METS = "HKAverageMETs" 2 | HK_ELEVATION_ASCENDED = "HKElevationAscended" 3 | HK_INDOOR_WORKOUT = "HKIndoorWorkout" 4 | HK_TIME_ZONE = "HKTimeZone" 5 | HK_WEATHER_HUMIDITY = "HKWeatherHumidity" 6 | HK_WEATHER_TEMPERATURE = "HKWeatherTemperature" 7 | HK_WORKOUT_BRAND_NAME = "HKWorkoutBrandName" 8 | HK_FOOD_MEAL = "HKFoodMeal" 9 | HK_FOOD_TYPE = "HKFoodType" 10 | HK_METADATA_KEY_HEART_RATE_MOTION_CONTEXT = "HKMetadataKeyHeartRateMotionContext" 11 | HK_SWIMMING_STROKE_STYLE = "HKSwimmingStrokeStyle" 12 | -------------------------------------------------------------------------------- /health/classes/correlation.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from health.classes.base import Sample 4 | from health.classes.record import Record 5 | from health.util import parse_float 6 | 7 | UNIT = "@unit" 8 | VALUE = "@value" 9 | 10 | 11 | class Correlation(Sample): 12 | def __init__(self, **data): 13 | super().__init__(**data) 14 | 15 | self.unit: str = data.get(UNIT) 16 | self.value: float = parse_float(data.get(VALUE)) 17 | 18 | self.records: List[Record] = list( 19 | map(lambda record_data: Record(**record_data), data.get("Record", [])) 20 | ) 21 | 22 | def __repr__(self) -> str: 23 | return f"{self.name}: {len(self.records)} records" 24 | -------------------------------------------------------------------------------- /health/classes/record.py: -------------------------------------------------------------------------------- 1 | from health.classes.base import Sample 2 | from health.util import parse_float, parse_time 3 | from typing import List 4 | 5 | UNIT = "@unit" 6 | VALUE = "@value" 7 | BPM = "@bpm" 8 | TIME = "@time" 9 | 10 | 11 | class HeartRateVariability: 12 | def __init__(self, **data): 13 | self.bpm = parse_float(data.get(BPM, 0.0)) 14 | self.timestamp = parse_time(data.get(TIME)) 15 | 16 | def __repr__(self) -> str: 17 | return f"{self.bpm:0.2f} bpm" 18 | 19 | 20 | class Record(Sample): 21 | def __init__(self, **data): 22 | super().__init__(**data) 23 | 24 | self.unit: str = data.get(UNIT) 25 | self.value: float = parse_float(data.get(VALUE)) 26 | self.heart_rate: List[HeartRateVariability] = [] 27 | 28 | def __repr__(self): 29 | return f"{self.name}: {self.value:0.2f} {self.unit} {self.created_at}" 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 5 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.8 23 | 24 | - name: Install Poetry 25 | uses: abatilo/actions-poetry@v2.0.0 26 | 27 | - name: Setup environment 28 | run: poetry install 29 | 30 | - name: Build and publish package 31 | run: poetry publish --build --username __token__ --password ${{ secrets.TOKEN_PYPI }} 32 | 33 | - name: Notify via Slack 34 | with: 35 | slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} 36 | pypi_project_name: apple-health 37 | uses: fedecalendino/slack-release-notifier@v1.2.1 38 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "apple-health" 3 | description = "Library to extract information from Apple Health exports." 4 | documentation = "https://github.com/fedecalendino/apple-health/blob/main/README.md" 5 | homepage = "https://github.com/fedecalendino/apple-health" 6 | license = "MIT" 7 | readme = "README.md" 8 | version = "2.0.0" 9 | 10 | authors = [ 11 | "Fede Calendino ", 12 | ] 13 | classifiers = [ 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python :: 3.8", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | ] 20 | keywords = [ 21 | "parser", 22 | "apple health", 23 | ] 24 | packages = [ 25 | { include = "health" }, 26 | ] 27 | 28 | [tool.poetry.dependencies] 29 | python = "^3.8" 30 | python-dateutil = "^2.8.2" 31 | xmltodict = "^0.13.0" 32 | 33 | [tool.poetry.dev-dependencies] 34 | coverage = "^6.4.1" 35 | black = "^22.6.0" 36 | ddt = "^1.5.0" 37 | 38 | [build-system] 39 | requires = ["poetry-core>=1.0.0"] 40 | build-backend = "poetry.core.masonry.api" 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Fede Calendino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /health/classes/me.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | DATE_OF_BIRTH = "@HKCharacteristicTypeIdentifierDateOfBirth" 5 | BIOLOGICAL_SEX = "@HKCharacteristicTypeIdentifierBiologicalSex" 6 | BLOOD_TYPE = "@HKCharacteristicTypeIdentifierBloodType" 7 | SKIN_TYPE = "@HKCharacteristicTypeIdentifierFitzpatrickSkinType" 8 | WHEELCHAIR_USE = "@HKCharacteristicTypeIdentifierWheelchairUse" 9 | 10 | 11 | class Me: 12 | def __init__(self, **data): 13 | try: 14 | self.date_of_birth: datetime = datetime.strptime( 15 | data.get(DATE_OF_BIRTH), "%Y-%m-%d" 16 | ) 17 | except (ValueError, TypeError): 18 | self.date_of_birth = None 19 | 20 | self.biological_sex: str = data.get(BIOLOGICAL_SEX) 21 | self.blood_type: str = data.get(BLOOD_TYPE) 22 | self.skin_type: str = data.get(SKIN_TYPE) 23 | self.wheelchair_use: bool = data.get(WHEELCHAIR_USE) 24 | 25 | @property 26 | def age(self) -> Optional[int]: 27 | if not self.date_of_birth: 28 | return None 29 | 30 | return (datetime.now() - self.date_of_birth).days // 365 31 | 32 | def __repr__(self): 33 | return f"{self.biological_sex}, {self.age} years old" 34 | -------------------------------------------------------------------------------- /health/constants/me.py: -------------------------------------------------------------------------------- 1 | HK_BIOLOGICAL_SEX_NOT_SET = "HKBiologicalSexNotSet" 2 | HK_BIOLOGICAL_SEX_FEMALE = "HKBiologicalSexFemale" 3 | HK_BIOLOGICAL_SEX_MALE = "HKBiologicalSexMale" 4 | HK_BIOLOGICAL_SEX_OTHER = "HKBiologicalSexOther" 5 | 6 | HK_BLOOD_TYPE_NOT_SET = "HKBloodTypeNotSet" 7 | HK_BLOOD_TYPE_O_POSITIVE = "HKBloodTypeOPositive" 8 | HK_BLOOD_TYPE_O_NEGATIVE = "HKBloodTypeONegative" 9 | HK_BLOOD_TYPE_A_POSITIVE = "HKBloodTypeAPositive" 10 | HK_BLOOD_TYPE_A_NEGATIVE = "HKBloodTypeANegative" 11 | HK_BLOOD_TYPE_B_POSITIVE = "HKBloodTypeBPositive" 12 | HK_BLOOD_TYPE_B_NEGATIVE = "HKBloodTypeBNegative" 13 | HK_BLOOD_TYPE_A_B_POSITIVE = "HKBloodTypeABPositive" 14 | HK_BLOOD_TYPE_A_B_NEGATIVE = "HKBloodTypeABNegative" 15 | 16 | HK_FITZPATRICK_SKIN_TYPE_NOT_SET = "HKFitzpatrickSkinTypeNotSet" 17 | HK_FITZPATRICK_SKIN_TYPE_I = "HKFitzpatrickSkinTypeI" 18 | HK_FITZPATRICK_SKIN_TYPE_I_I = "HKFitzpatrickSkinTypeII" 19 | HK_FITZPATRICK_SKIN_TYPE_I_I_I = "HKFitzpatrickSkinTypeIII" 20 | HK_FITZPATRICK_SKIN_TYPE_I_V = "HKFitzpatrickSkinTypeIV" 21 | HK_FITZPATRICK_SKIN_TYPE_V = "HKFitzpatrickSkinTypeV" 22 | HK_FITZPATRICK_SKIN_TYPE_V_I = "HKFitzpatrickSkinTypeVI" 23 | 24 | HK_WHEELCHAIR_USE_NOT_SET = "HKWheelchairUseNotSet" 25 | HK_WHEELCHAIR_USE_NO = "HKWheelchairUseNo" 26 | HK_WHEELCHAIR_USE_YES = "HKWheelchairUseYes" 27 | -------------------------------------------------------------------------------- /health/classes/base.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from health.util import parse_date 4 | 5 | 6 | TYPE = "@type" 7 | SOURCE_NAME = "@sourceName" 8 | CREATION_DATE = "@creationDate" 9 | START_DATE = "@startDate" 10 | END_DATE = "@endDate" 11 | 12 | KEY = "@key" 13 | VALUE = "@value" 14 | 15 | 16 | class MetaData: 17 | def __init__(self, **data): 18 | self.key = data.get(KEY) 19 | self.value = data.get(VALUE) 20 | 21 | def __repr__(self) -> str: 22 | return f"{self.key}: {self.value}" 23 | 24 | 25 | class Sample: 26 | NAME_KEY = TYPE 27 | 28 | def __init__(self, **data): 29 | self.name: str = data[self.NAME_KEY] 30 | 31 | self.source: str = data.get(SOURCE_NAME) 32 | 33 | self.created_at: datetime = parse_date(data.get(CREATION_DATE)) 34 | self.start: datetime = parse_date(data.get(START_DATE)) 35 | self.end: datetime = parse_date(data.get(END_DATE)) 36 | 37 | metadata = data.get("MetadataEntry") 38 | 39 | if metadata is None: 40 | self.metadata = [] 41 | elif isinstance(metadata, dict): 42 | self.metadata = [MetaData(**metadata)] 43 | elif isinstance(metadata, list): 44 | self.metadata = list(map(lambda m: MetaData(**m), metadata)) 45 | 46 | @property 47 | def seconds(self) -> int: 48 | return (self.end - self.start).seconds 49 | -------------------------------------------------------------------------------- /health/classes/workout.py: -------------------------------------------------------------------------------- 1 | from health.classes.base import Sample 2 | from health.util import parse_float 3 | 4 | WORKOUT_ACTIVITY_TYPE = "@workoutActivityType" 5 | DURATION = "@duration" 6 | DURATION_UNIT = "@durationUnit" 7 | TOTAL_DISTANCE = "@totalDistance" 8 | TOTAL_DISTANCE_UNIT = "@totalDistanceUnit" 9 | TOTAL_ENERGY_BURNED = "@totalEnergyBurned" 10 | TOTAL_ENERGY_BURNED_UNIT = "@totalEnergyBurnedUnit" 11 | TOTAL_FLIGHTS_CLIMBED = "@totalFlightsClimbed" 12 | TOTAL_SWIMMING_STROKE_COUNT = "@totalSwimmingStrokeCount" 13 | 14 | 15 | class Workout(Sample): 16 | NAME_KEY = WORKOUT_ACTIVITY_TYPE 17 | 18 | def __init__(self, **data): 19 | super().__init__(**data) 20 | 21 | self.duration: float = parse_float(data.get(DURATION)) 22 | self.duration_unit: str = data.get(DURATION_UNIT) 23 | 24 | self.distance: float = parse_float(data.get(TOTAL_DISTANCE)) 25 | self.distance_unit: str = data.get(TOTAL_DISTANCE_UNIT) 26 | 27 | self.energy_burned: float = parse_float(data.get(TOTAL_ENERGY_BURNED)) 28 | self.energy_burned_unit: str = data.get(TOTAL_ENERGY_BURNED_UNIT) 29 | 30 | self.flights_climbed: float = parse_float(data.get(TOTAL_FLIGHTS_CLIMBED)) 31 | self.swimming_strokes: float = parse_float( 32 | data.get(TOTAL_SWIMMING_STROKE_COUNT) 33 | ) 34 | 35 | def __repr__(self) -> str: 36 | return f"{self.name}: {self.duration:0.2f} {self.duration_unit}" 37 | -------------------------------------------------------------------------------- /health/util.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, time 2 | from typing import Optional 3 | 4 | from dateutil import parser 5 | 6 | from health.constants import record 7 | 8 | CONSTANTS = { 9 | record.HK_RECORD_NOT_APPLICABLE: 0, 10 | record.HK_RECORD_APPLE_STAND_HOUR_STOOD: 0, 11 | record.HK_RECORD_APPLE_STAND_HOUR_IDLE: 1, 12 | record.HK_RECORD_AUDIO_EXPOSURE_EVENT_LOUD_ENVIRONMENT: 1, 13 | record.HK_RECORD_CERVICAL_MUCUS_QUALITY_DRY: 1, 14 | record.HK_RECORD_CERVICAL_MUCUS_QUALITY_STICKY: 2, 15 | record.HK_RECORD_CERVICAL_MUCUS_QUALITY_CREAMY: 3, 16 | record.HK_RECORD_CERVICAL_MUCUS_QUALITY_WATERY: 4, 17 | record.HK_RECORD_CERVICAL_MUCUS_QUALITY_EGG_WHITE: 5, 18 | record.HK_RECORD_OVULATION_TEST_RESULT_NEGATIVE: 1, 19 | record.HK_RECORD_OVULATION_TEST_RESULT_LUTEINIZING_HORMONE_SURGE: 2, 20 | record.HK_RECORD_OVULATION_TEST_RESULT_INDETERMINATE: 3, 21 | record.HK_RECORD_OVULATION_TEST_RESULT_ESTROGEN_SURGE: 4, 22 | record.HK_RECORD_SLEEP_ANALYSIS_IN_BED: 0, 23 | record.HK_RECORD_SLEEP_ANALYSIS_ASLEEP: 1, 24 | record.HK_RECORD_SLEEP_ANALYSIS_AWAKE: 2, 25 | } 26 | 27 | 28 | def parse_date(value: str) -> Optional[datetime]: 29 | if value is None: 30 | return None 31 | 32 | return parser.parse(value) 33 | 34 | 35 | def parse_time(value: str) -> Optional[time]: 36 | if value is None: 37 | return None 38 | 39 | return parser.parse(value).time() 40 | 41 | 42 | def parse_float(value: str, default: float = 0.0) -> float: 43 | if value is None: 44 | return default 45 | 46 | if value in CONSTANTS: 47 | return CONSTANTS[value] 48 | 49 | try: 50 | return float(value) 51 | except (ValueError, TypeError): 52 | return default 53 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Push to main branch 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ["3.8", "3.9", "3.10"] 14 | timeout-minutes: 5 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install Poetry 28 | uses: abatilo/actions-poetry@v2.0.0 29 | 30 | - name: Setup environment 31 | run: poetry install 32 | 33 | - name: Run tests with Coverage 34 | run: | 35 | poetry run coverage run --source health -m unittest discover 36 | poetry run coverage xml -o ./coverage.xml 37 | 38 | - name: Fix coverage xml path 39 | run: | 40 | sed -i "s@${GITHUB_WORKSPACE}@/github/workspace/@g" ./coverage.xml 41 | 42 | - name: SonarCloud scan 43 | if: matrix.python-version == '3.8' 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} 46 | SONAR_TOKEN: ${{ secrets.TOKEN_SONAR }} 47 | uses: sonarsource/sonarcloud-github-action@master 48 | 49 | validate: 50 | runs-on: ubuntu-latest 51 | timeout-minutes: 5 52 | needs: test 53 | 54 | steps: 55 | - name: Checkout code 56 | uses: actions/checkout@v2 57 | with: 58 | fetch-depth: 0 59 | 60 | - name: Setup Python 61 | uses: actions/setup-python@v2 62 | with: 63 | python-version: 3.8 64 | 65 | - name: Install Poetry 66 | uses: abatilo/actions-poetry@v2.0.0 67 | 68 | - name: Setup environment 69 | run: poetry install 70 | 71 | - name: Build package 72 | run: poetry build 73 | -------------------------------------------------------------------------------- /health/data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import xmltodict 4 | 5 | from health.classes import ( 6 | Me, 7 | ActivitySummary, 8 | Correlation, 9 | Record, 10 | Workout, 11 | ) 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class HealthData: 17 | def __init__(self): 18 | self.me = Me() 19 | self.activity_summaries = [] 20 | self.correlations = [] 21 | self.records = [] 22 | self.workouts = [] 23 | 24 | @staticmethod 25 | def read( 26 | file_name: str, 27 | include_me: bool = True, 28 | include_activity_summaries: bool = True, 29 | include_correlations: bool = True, 30 | include_records: bool = True, 31 | include_workouts: bool = True, 32 | ) -> "HealthData": 33 | with open(file_name) as file: 34 | xml = xmltodict.parse(file.read()) 35 | data = xml["HealthData"] 36 | 37 | health_data = HealthData() 38 | 39 | if include_me: 40 | log.info("Reading: Me...") 41 | health_data.me = Me(**data["Me"]) 42 | 43 | if include_activity_summaries: 44 | log.info("Reading: ActivitySummary...") 45 | health_data.activity_summaries = list( 46 | map(lambda a: ActivitySummary(**a), data.get("ActivitySummary", [])) 47 | ) 48 | 49 | if include_correlations: 50 | log.info("Reading: Correlation...") 51 | health_data.correlations = list( 52 | map(lambda c: Correlation(**c), data.get("Correlation", [])) 53 | ) 54 | 55 | if include_records: 56 | log.info("Reading: Record...") 57 | health_data.records = list( 58 | map(lambda r: Record(**r), data.get("Record", [])) 59 | ) 60 | 61 | if include_workouts: 62 | log.info("Reading: Workout...") 63 | health_data.workouts = list( 64 | map(lambda w: Workout(**w), data.get("Workout", [])) 65 | ) 66 | 67 | return health_data 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apple-health 2 | 3 | [![Version](https://img.shields.io/pypi/v/apple-health?logo=pypi)](https://pypi.org/project/apple-health) 4 | [![Quality Gate Status](https://img.shields.io/sonar/alert_status/fedecalendino_apple-health?logo=sonarcloud&server=https://sonarcloud.io)](https://sonarcloud.io/dashboard?id=fedecalendino_apple-health) 5 | [![CodeCoverage](https://img.shields.io/sonar/coverage/fedecalendino_apple-health?logo=sonarcloud&server=https://sonarcloud.io)](https://sonarcloud.io/dashboard?id=fedecalendino_apple-health) 6 | 7 | 8 | Library to extract information from Apple Health exports. 9 | 10 | --- 11 | 12 | ## Setup 13 | 14 | To use this library, is required to provide an export file from the iOS Apple Health app. 15 | 16 | ### How to get the export 17 | 18 | 1. Open the Apple Health app on your iOS device. 19 | 2. Tap on your profile picture on the top-right corner. 20 | 3. Scroll down until you see a button that reads "Export All Health Data". 21 | 4. After pressing the button, a dialog will appear while the export process is ongoing (it might take a while). 22 | 5. Once the process is finished, a file called `apple_health_export.zip` will be generated. 23 | 6. Finally, from that zip file you'll need only the file named `export.xml`. 24 | 25 | 26 | ## Usage 27 | 28 | ```python 29 | from health import HealthData 30 | 31 | FILE = "./export/export.xml" 32 | data = HealthData.read( 33 | FILE, 34 | include_me=True, 35 | include_activity_summaries=True, 36 | include_correlations=False, 37 | include_records=False, 38 | include_workouts=True, 39 | ) 40 | 41 | print(data.me.biological_sex) 42 | print(f"{len(data.activity_summaries)} activity records") 43 | print(f"{len(data.correlations)} correlations") 44 | print(f"{len(data.records)} records") 45 | print(f"{len(data.workouts)} workouts") 46 | ``` 47 | 48 | ```text 49 | >> HKBiologicalSexMale 50 | >> 322 activity records 51 | >> 0 correlations 52 | >> 0 records 53 | >> 129 workouts 54 | ``` 55 | 56 | > note: use the flags on the `HealthData.read` to include only what you need to speed up the reading process. -------------------------------------------------------------------------------- /health/classes/activity_summary.py: -------------------------------------------------------------------------------- 1 | from health.util import parse_date, parse_float 2 | 3 | DATE_COMPONENTS = "@dateComponents" 4 | ACTIVE_ENERGY_BURNED = "@activeEnergyBurned" 5 | ACTIVE_ENERGY_BURNED_GOAL = "@activeEnergyBurnedGoal" 6 | ACTIVE_ENERGY_BURNED_UNIT = "@activeEnergyBurnedUnit" 7 | APPLE_EXERCISE_TIME = "@appleExerciseTime" 8 | APPLE_EXERCISE_TIME_GOAL = "@appleExerciseTimeGoal" 9 | APPLE_STAND_HOURS = "@appleStandHours" 10 | APPLE_STAND_HOURS_GOAL = "@appleStandHoursGoal" 11 | 12 | 13 | class ActivitySummary: 14 | # a.k.a. The Rings 15 | 16 | def __init__(self, **data): 17 | self.date = parse_date(data.get(DATE_COMPONENTS)) 18 | 19 | # Red 20 | self.active_energy_burned: float = parse_float(data.get(ACTIVE_ENERGY_BURNED)) 21 | self.active_energy_burned_goal: float = parse_float( 22 | data.get(ACTIVE_ENERGY_BURNED_GOAL) 23 | ) 24 | self.active_energy_burned_unit: str = data.get( 25 | ACTIVE_ENERGY_BURNED_UNIT, "kcal" 26 | ) 27 | 28 | # Green 29 | self.exercise_time: float = parse_float(data.get(APPLE_EXERCISE_TIME)) 30 | self.exercise_time_goal: float = parse_float(data.get(APPLE_EXERCISE_TIME_GOAL)) 31 | 32 | # Blue 33 | self.stand_hours: float = parse_float(data.get(APPLE_STAND_HOURS)) 34 | self.stand_hours_goal: float = parse_float(data.get(APPLE_STAND_HOURS_GOAL)) 35 | 36 | @property 37 | def active_energy_percent(self) -> float: 38 | if not self.active_energy_burned_goal: 39 | return 0.0 40 | 41 | return self.active_energy_burned / self.active_energy_burned_goal 42 | 43 | @property 44 | def exercise_time_percent(self) -> float: 45 | if not self.exercise_time_goal: 46 | return 0.0 47 | 48 | return self.exercise_time / self.exercise_time_goal 49 | 50 | @property 51 | def stand_hours_percent(self) -> float: 52 | if not self.stand_hours_goal: 53 | return 0.0 54 | 55 | return self.stand_hours / self.stand_hours_goal 56 | 57 | def __repr__(self) -> str: 58 | aep = int(100 * self.active_energy_percent) 59 | etp = int(100 * self.exercise_time_percent) 60 | shp = int(100 * self.stand_hours_percent) 61 | 62 | return f"{aep}% / {etp}% / {shp}%" 63 | -------------------------------------------------------------------------------- /health/constants/workout.py: -------------------------------------------------------------------------------- 1 | HK_WORKOUT_AMERICAN_FOOTBALL = "HKWorkoutActivityTypeAmericanFootball" 2 | HK_WORKOUT_ARCHERY = "HKWorkoutActivityTypeArchery" 3 | HK_WORKOUT_AUSTRALIAN_FOOTBALL = "HKWorkoutActivityTypeAustralianFootball" 4 | HK_WORKOUT_BADMINTON = "HKWorkoutActivityTypeBadminton" 5 | HK_WORKOUT_BASEBALL = "HKWorkoutActivityTypeBaseball" 6 | HK_WORKOUT_BASKETBALL = "HKWorkoutActivityTypeBasketball" 7 | HK_WORKOUT_BOWLING = "HKWorkoutActivityTypeBowling" 8 | HK_WORKOUT_BOXING = "HKWorkoutActivityTypeBoxing" 9 | HK_WORKOUT_CLIMBING = "HKWorkoutActivityTypeClimbing" 10 | HK_WORKOUT_CRICKET = "HKWorkoutActivityTypeCricket" 11 | HK_WORKOUT_CROSS_TRAINING = "HKWorkoutActivityTypeCrossTraining" 12 | HK_WORKOUT_CURLING = "HKWorkoutActivityTypeCurling" 13 | HK_WORKOUT_CYCLING = "HKWorkoutActivityTypeCycling" 14 | HK_WORKOUT_DANCE = "HKWorkoutActivityTypeDance" 15 | HK_WORKOUT_DANCE_INSPIRED_TRAINING = "HKWorkoutActivityTypeDanceInspiredTraining" 16 | HK_WORKOUT_ELLIPTICAL = "HKWorkoutActivityTypeElliptical" 17 | HK_WORKOUT_EQUESTRIAN_SPORTS = "HKWorkoutActivityTypeEquestrianSports" 18 | HK_WORKOUT_FENCING = "HKWorkoutActivityTypeFencing" 19 | HK_WORKOUT_FISHING = "HKWorkoutActivityTypeFishing" 20 | HK_WORKOUT_FUNCTIONAL_STRENGTH_TRAINING = ( 21 | "HKWorkoutActivityTypeFunctionalStrengthTraining" 22 | ) 23 | HK_WORKOUT_GOLF = "HKWorkoutActivityTypeGolf" 24 | HK_WORKOUT_GYMNASTICS = "HKWorkoutActivityTypeGymnastics" 25 | HK_WORKOUT_HANDBALL = "HKWorkoutActivityTypeHandball" 26 | HK_WORKOUT_HIGH_INTENSITY_INTERVAL_TRAINING = ( 27 | "HKWorkoutActivityTypeHighIntensityIntervalTraining" 28 | ) 29 | HK_WORKOUT_HIKING = "HKWorkoutActivityTypeHiking" 30 | HK_WORKOUT_HOCKEY = "HKWorkoutActivityTypeHockey" 31 | HK_WORKOUT_HUNTING = "HKWorkoutActivityTypeHunting" 32 | HK_WORKOUT_LACROSSE = "HKWorkoutActivityTypeLacrosse" 33 | HK_WORKOUT_MARTIAL_ARTS = "HKWorkoutActivityTypeMartialArts" 34 | HK_WORKOUT_MIND_AND_BODY = "HKWorkoutActivityTypeMindAndBody" 35 | HK_WORKOUT_MIXED_METABOLIC_CARDIO_TRAINING = ( 36 | "HKWorkoutActivityTypeMixedMetabolicCardioTraining" 37 | ) 38 | HK_WORKOUT_OTHER = "HKWorkoutActivityTypeOther" 39 | HK_WORKOUT_PADDLE_SPORTS = "HKWorkoutActivityTypePaddleSports" 40 | HK_WORKOUT_PLAY = "HKWorkoutActivityTypePlay" 41 | HK_WORKOUT_PREPARATION_AND_RECOVERY = "HKWorkoutActivityTypePreparationAndRecovery" 42 | HK_WORKOUT_RACQUETBALL = "HKWorkoutActivityTypeRacquetball" 43 | HK_WORKOUT_ROWING = "HKWorkoutActivityTypeRowing" 44 | HK_WORKOUT_RUGBY = "HKWorkoutActivityTypeRugby" 45 | HK_WORKOUT_RUNNING = "HKWorkoutActivityTypeRunning" 46 | HK_WORKOUT_SAILING = "HKWorkoutActivityTypeSailing" 47 | HK_WORKOUT_SKATING_SPORTS = "HKWorkoutActivityTypeSkatingSports" 48 | HK_WORKOUT_SNOW_SPORTS = "HKWorkoutActivityTypeSnowSports" 49 | HK_WORKOUT_SOCCER = "HKWorkoutActivityTypeSoccer" 50 | HK_WORKOUT_SOFTBALL = "HKWorkoutActivityTypeSoftball" 51 | HK_WORKOUT_SQUASH = "HKWorkoutActivityTypeSquash" 52 | HK_WORKOUT_STAIR_CLIMBING = "HKWorkoutActivityTypeStairClimbing" 53 | HK_WORKOUT_SURFING_SPORTS = "HKWorkoutActivityTypeSurfingSports" 54 | HK_WORKOUT_SWIMMING = "HKWorkoutActivityTypeSwimming" 55 | HK_WORKOUT_TABLE_TENNIS = "HKWorkoutActivityTypeTableTennis" 56 | HK_WORKOUT_TENNIS = "HKWorkoutActivityTypeTennis" 57 | HK_WORKOUT_TRACK_AND_FIELD = "HKWorkoutActivityTypeTrackAndField" 58 | HK_WORKOUT_TRADITIONAL_STRENGTH_TRAINING = ( 59 | "HKWorkoutActivityTypeTraditionalStrengthTraining" 60 | ) 61 | HK_WORKOUT_VOLLEYBALL = "HKWorkoutActivityTypeVolleyball" 62 | HK_WORKOUT_WALKING = "HKWorkoutActivityTypeWalking" 63 | HK_WORKOUT_WATER_FITNESS = "HKWorkoutActivityTypeWaterFitness" 64 | HK_WORKOUT_WATER_POLO = "HKWorkoutActivityTypeWaterPolo" 65 | HK_WORKOUT_WATER_SPORTS = "HKWorkoutActivityTypeWaterSports" 66 | HK_WORKOUT_WRESTLING = "HKWorkoutActivityTypeWrestling" 67 | HK_WORKOUT_YOGA = "HKWorkoutActivityTypeYoga" 68 | -------------------------------------------------------------------------------- /health/constants/record.py: -------------------------------------------------------------------------------- 1 | HK_RECORD_ACTIVE_ENERGY_BURNED = "HKQuantityTypeIdentifierActiveEnergyBurned" 2 | HK_RECORD_APPLE_EXERCISE_TIME = "HKQuantityTypeIdentifierAppleExerciseTime" 3 | HK_RECORD_APPLE_STAND_HOUR = "HKCategoryTypeIdentifierAppleStandHour" 4 | HK_RECORD_APPLE_STAND_HOUR_IDLE = "HKCategoryValueAppleStandHourIdle" 5 | HK_RECORD_APPLE_STAND_HOUR_STOOD = "HKCategoryValueAppleStandHourStood" 6 | HK_RECORD_APPLE_STAND_TIME = "HKQuantityTypeIdentifierAppleStandTime" 7 | HK_RECORD_AUDIO_EXPOSURE_EVENT_LOUD_ENVIRONMENT = ( 8 | "HKCategoryValueAudioExposureEventLoudEnvironment" 9 | ) 10 | HK_RECORD_BASAL_BODY_TEMPERATURE = "HKQuantityTypeIdentifierBasalBodyTemperature" 11 | HK_RECORD_BASAL_ENERGY_BURNED = "HKQuantityTypeIdentifierBasalEnergyBurned" 12 | HK_RECORD_BLOOD_ALCOHOL_CONTENT = "HKQuantityTypeIdentifierBloodAlcoholContent" 13 | HK_RECORD_BLOOD_GLUCOSE = "HKQuantityTypeIdentifierBloodGlucose" 14 | HK_RECORD_BLOOD_PRESSURE_DIASTOLIC = "HKQuantityTypeIdentifierBloodPressureDiastolic" 15 | HK_RECORD_BLOOD_PRESSURE_SYSTOLIC = "HKQuantityTypeIdentifierBloodPressureSystolic" 16 | HK_RECORD_BODY_FAT_PERCENTAGE = "HKQuantityTypeIdentifierBodyFatPercentage" 17 | HK_RECORD_BODY_MASS = "HKQuantityTypeIdentifierBodyMass" 18 | HK_RECORD_BODY_MASS_INDEX = "HKQuantityTypeIdentifierBodyMassIndex" 19 | HK_RECORD_BODY_TEMPERATURE = "HKQuantityTypeIdentifierBodyTemperature" 20 | HK_RECORD_CERVICAL_MUCUS_QUALITY_CREAMY = "HKCategoryValueCervicalMucusQualityCreamy" 21 | HK_RECORD_CERVICAL_MUCUS_QUALITY_DRY = "HKCategoryValueCervicalMucusQualityDry" 22 | HK_RECORD_CERVICAL_MUCUS_QUALITY_EGG_WHITE = ( 23 | "HKCategoryValueCervicalMucusQualityEggWhite" 24 | ) 25 | HK_RECORD_CERVICAL_MUCUS_QUALITY_STICKY = "HKCategoryValueCervicalMucusQualitySticky" 26 | HK_RECORD_CERVICAL_MUCUS_QUALITY_WATERY = "HKCategoryValueCervicalMucusQualityWatery" 27 | HK_RECORD_DIETARY_BIOTIN = "HKQuantityTypeIdentifierDietaryBiotin" 28 | HK_RECORD_DIETARY_CAFFEINE = "HKQuantityTypeIdentifierDietaryCaffeine" 29 | HK_RECORD_DIETARY_CALCIUM = "HKQuantityTypeIdentifierDietaryCalcium" 30 | HK_RECORD_DIETARY_CARBOHYDRATES = "HKQuantityTypeIdentifierDietaryCarbohydrates" 31 | HK_RECORD_DIETARY_CHLORIDE = "HKQuantityTypeIdentifierDietaryChloride" 32 | HK_RECORD_DIETARY_CHOLESTEROL = "HKQuantityTypeIdentifierDietaryCholesterol" 33 | HK_RECORD_DIETARY_CHROMIUM = "HKQuantityTypeIdentifierDietaryChromium" 34 | HK_RECORD_DIETARY_COPPER = "HKQuantityTypeIdentifierDietaryCopper" 35 | HK_RECORD_DIETARY_ENERGY_CONSUMED = "HKQuantityTypeIdentifierDietaryEnergyConsumed" 36 | HK_RECORD_DIETARY_FAT_MONOUNSATURATED = ( 37 | "HKQuantityTypeIdentifierDietaryFatMonounsaturated" 38 | ) 39 | HK_RECORD_DIETARY_FAT_POLYUNSATURATED = ( 40 | "HKQuantityTypeIdentifierDietaryFatPolyunsaturated" 41 | ) 42 | HK_RECORD_DIETARY_FAT_SATURATED = "HKQuantityTypeIdentifierDietaryFatSaturated" 43 | HK_RECORD_DIETARY_FAT_TOTAL = "HKQuantityTypeIdentifierDietaryFatTotal" 44 | HK_RECORD_DIETARY_FIBER = "HKQuantityTypeIdentifierDietaryFiber" 45 | HK_RECORD_DIETARY_FOLATE = "HKQuantityTypeIdentifierDietaryFolate" 46 | HK_RECORD_DIETARY_IODINE = "HKQuantityTypeIdentifierDietaryIodine" 47 | HK_RECORD_DIETARY_IRON = "HKQuantityTypeIdentifierDietaryIron" 48 | HK_RECORD_DIETARY_MAGNESIUM = "HKQuantityTypeIdentifierDietaryMagnesium" 49 | HK_RECORD_DIETARY_MANGANESE = "HKQuantityTypeIdentifierDietaryManganese" 50 | HK_RECORD_DIETARY_MOLYBDENUM = "HKQuantityTypeIdentifierDietaryMolybdenum" 51 | HK_RECORD_DIETARY_NIACIN = "HKQuantityTypeIdentifierDietaryNiacin" 52 | HK_RECORD_DIETARY_PANTOTHENIC_ACID = "HKQuantityTypeIdentifierDietaryPantothenicAcid" 53 | HK_RECORD_DIETARY_PHOSPHORUS = "HKQuantityTypeIdentifierDietaryPhosphorus" 54 | HK_RECORD_DIETARY_POTASSIUM = "HKQuantityTypeIdentifierDietaryPotassium" 55 | HK_RECORD_DIETARY_PROTEIN = "HKQuantityTypeIdentifierDietaryProtein" 56 | HK_RECORD_DIETARY_RIBOFLAVIN = "HKQuantityTypeIdentifierDietaryRiboflavin" 57 | HK_RECORD_DIETARY_SELENIUM = "HKQuantityTypeIdentifierDietarySelenium" 58 | HK_RECORD_DIETARY_SODIUM = "HKQuantityTypeIdentifierDietarySodium" 59 | HK_RECORD_DIETARY_SUGAR = "HKQuantityTypeIdentifierDietarySugar" 60 | HK_RECORD_DIETARY_THIAMIN = "HKQuantityTypeIdentifierDietaryThiamin" 61 | HK_RECORD_DIETARY_VITAMIN_A = "HKQuantityTypeIdentifierDietaryVitaminA" 62 | HK_RECORD_DIETARY_VITAMIN_B12 = "HKQuantityTypeIdentifierDietaryVitaminB12" 63 | HK_RECORD_DIETARY_VITAMIN_B6 = "HKQuantityTypeIdentifierDietaryVitaminB6" 64 | HK_RECORD_DIETARY_VITAMIN_C = "HKQuantityTypeIdentifierDietaryVitaminC" 65 | HK_RECORD_DIETARY_VITAMIN_D = "HKQuantityTypeIdentifierDietaryVitaminD" 66 | HK_RECORD_DIETARY_VITAMIN_E = "HKQuantityTypeIdentifierDietaryVitaminE" 67 | HK_RECORD_DIETARY_VITAMIN_K = "HKQuantityTypeIdentifierDietaryVitaminK" 68 | HK_RECORD_DIETARY_WATER = "HKQuantityTypeIdentifierDietaryWater" 69 | HK_RECORD_DIETARY_ZINC = "HKQuantityTypeIdentifierDietaryZinc" 70 | HK_RECORD_DISTANCE_CYCLING = "HKQuantityTypeIdentifierDistanceCycling" 71 | HK_RECORD_DISTANCE_DOWNHILL_SNOW_SPORTS = ( 72 | "HKQuantityTypeIdentifierDistanceDownhillSnowSports" 73 | ) 74 | HK_RECORD_DISTANCE_SWIMMING = "HKQuantityTypeIdentifierDistanceSwimming" 75 | HK_RECORD_DISTANCE_WALKING_RUNNING = "HKQuantityTypeIdentifierDistanceWalkingRunning" 76 | HK_RECORD_DISTANCE_WHEELCHAIR = "HKQuantityTypeIdentifierDistanceWheelchair" 77 | HK_RECORD_ELECTRODERMAL_ACTIVITY = "HKQuantityTypeIdentifierElectrodermalActivity" 78 | HK_RECORD_ENVIRONMENTAL_AUDIO_EXPOSURE = ( 79 | "HKQuantityTypeIdentifierEnvironmentalAudioExposure" 80 | ) 81 | HK_RECORD_FLIGHTS_CLIMBED = "HKQuantityTypeIdentifierFlightsClimbed" 82 | HK_RECORD_FORCED_EXPIRATORY_VOLUME1 = "HKQuantityTypeIdentifierForcedExpiratoryVolume1" 83 | HK_RECORD_FORCED_VITAL_CAPACITY = "HKQuantityTypeIdentifierForcedVitalCapacity" 84 | HK_RECORD_HEADPHONE_AUDIO_EXPOSURE = "HKQuantityTypeIdentifierHeadphoneAudioExposure" 85 | HK_RECORD_HEART_RATE = "HKQuantityTypeIdentifierHeartRate" 86 | HK_RECORD_HEART_RATE_VARIABILITY_S_D_N_N = ( 87 | "HKQuantityTypeIdentifierHeartRateVariabilitySDNN" 88 | ) 89 | HK_RECORD_HEIGHT = "HKQuantityTypeIdentifierHeight" 90 | HK_RECORD_INHALER_USAGE = "HKQuantityTypeIdentifierInhalerUsage" 91 | HK_RECORD_INSULIN_DELIVERY = "HKQuantityTypeIdentifierInsulinDelivery" 92 | HK_RECORD_LEAN_BODY_MASS = "HKQuantityTypeIdentifierLeanBodyMass" 93 | HK_RECORD_MINDFUL_SESSION = "HKCategoryTypeIdentifierMindfulSession" 94 | HK_RECORD_NIKE_FUEL = "HKQuantityTypeIdentifierNikeFuel" 95 | HK_RECORD_NOT_APPLICABLE = "HKCategoryValueNotApplicable" 96 | HK_RECORD_NUMBER_OF_TIMES_FALLEN = "HKQuantityTypeIdentifierNumberOfTimesFallen" 97 | HK_RECORD_OVULATION_TEST_RESULT_ESTROGEN_SURGE = ( 98 | "HKCategoryValueOvulationTestResultEstrogenSurge" 99 | ) 100 | HK_RECORD_OVULATION_TEST_RESULT_INDETERMINATE = ( 101 | "HKCategoryValueOvulationTestResultIndeterminate" 102 | ) 103 | HK_RECORD_OVULATION_TEST_RESULT_LUTEINIZING_HORMONE_SURGE = ( 104 | "HKCategoryValueOvulationTestResultLuteinizingHormoneSurge" 105 | ) 106 | HK_RECORD_OVULATION_TEST_RESULT_NEGATIVE = "HKCategoryValueOvulationTestResultNegative" 107 | HK_RECORD_OXYGEN_SATURATION = "HKQuantityTypeIdentifierOxygenSaturation" 108 | HK_RECORD_PEAK_EXPIRATORY_FLOW_RATE = "HKQuantityTypeIdentifierPeakExpiratoryFlowRate" 109 | HK_RECORD_PERIPHERAL_PERFUSION_INDEX = ( 110 | "HKQuantityTypeIdentifierPeripheralPerfusionIndex" 111 | ) 112 | HK_RECORD_PUSH_COUNT = "HKQuantityTypeIdentifierPushCount" 113 | HK_RECORD_RESPIRATORY_RATE = "HKQuantityTypeIdentifierRespiratoryRate" 114 | HK_RECORD_RESTING_HEART_RATE = "HKQuantityTypeIdentifierRestingHeartRate" 115 | HK_RECORD_SLEEP_ANALYSIS = "HKCategoryTypeIdentifierSleepAnalysis" 116 | HK_RECORD_SLEEP_ANALYSIS_ASLEEP = "HKCategoryValueSleepAnalysisAsleep" 117 | HK_RECORD_SLEEP_ANALYSIS_AWAKE = "HKCategoryValueSleepAnalysisAwake" 118 | HK_RECORD_SLEEP_ANALYSIS_IN_BED = "HKCategoryValueSleepAnalysisInBed" 119 | HK_RECORD_STEP_COUNT = "HKQuantityTypeIdentifierStepCount" 120 | HK_RECORD_SWIMMING_STROKE_COUNT = "HKQuantityTypeIdentifierSwimmingStrokeCount" 121 | HK_RECORD_TOOTHBRUSHING_EVENT = "HKCategoryTypeIdentifierToothbrushingEvent" 122 | HK_RECORD_U_V_EXPOSURE = "HKQuantityTypeIdentifierUVExposure" 123 | HK_RECORD_V_O2_MAX = "HKQuantityTypeIdentifierVO2Max" 124 | HK_RECORD_WAIST_CIRCUMFERENCE = "HKQuantityTypeIdentifierWaistCircumference" 125 | HK_RECORD_WALKING_HEART_RATE_AVERAGE = "HKQuantityTypeIdentifierWalkingHeartRateAverage" 126 | HK_RECORD_WALKING_DOUBLE_SUPPORT_PERCENTAGE = ( 127 | "HKQuantityTypeIdentifierWalkingDoubleSupportPercentage" 128 | ) 129 | HK_RECORD_SIX_MINUTE_WALK_TEST_DISTANCE = ( 130 | "HKQuantityTypeIdentifierSixMinuteWalkTestDistance" 131 | ) 132 | HK_RECORD_WALKING_SPEED = "HKQuantityTypeIdentifierWalkingSpeed" 133 | HK_RECORD_WALKING_STEP_LENGTH = "HKQuantityTypeIdentifierWalkingStepLength" 134 | HK_RECORD_WALKING_ASYMMETRY_PERCENTAGE = ( 135 | "HKQuantityTypeIdentifierWalkingAsymmetryPercentage" 136 | ) 137 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "black" 3 | version = "22.6.0" 4 | description = "The uncompromising code formatter." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.6.2" 8 | 9 | [package.dependencies] 10 | click = ">=8.0.0" 11 | mypy-extensions = ">=0.4.3" 12 | pathspec = ">=0.9.0" 13 | platformdirs = ">=2" 14 | tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} 15 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 16 | 17 | [package.extras] 18 | colorama = ["colorama (>=0.4.3)"] 19 | d = ["aiohttp (>=3.7.4)"] 20 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 21 | uvloop = ["uvloop (>=0.15.2)"] 22 | 23 | [[package]] 24 | name = "click" 25 | version = "8.1.3" 26 | description = "Composable command line interface toolkit" 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=3.7" 30 | 31 | [package.dependencies] 32 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 33 | 34 | [[package]] 35 | name = "colorama" 36 | version = "0.4.5" 37 | description = "Cross-platform colored terminal text." 38 | category = "dev" 39 | optional = false 40 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 41 | 42 | [[package]] 43 | name = "coverage" 44 | version = "6.4.1" 45 | description = "Code coverage measurement for Python" 46 | category = "dev" 47 | optional = false 48 | python-versions = ">=3.7" 49 | 50 | [package.extras] 51 | toml = ["tomli"] 52 | 53 | [[package]] 54 | name = "ddt" 55 | version = "1.5.0" 56 | description = "Data-Driven/Decorated Tests" 57 | category = "dev" 58 | optional = false 59 | python-versions = "*" 60 | 61 | [[package]] 62 | name = "mypy-extensions" 63 | version = "0.4.3" 64 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 65 | category = "dev" 66 | optional = false 67 | python-versions = "*" 68 | 69 | [[package]] 70 | name = "pathspec" 71 | version = "0.9.0" 72 | description = "Utility library for gitignore style pattern matching of file paths." 73 | category = "dev" 74 | optional = false 75 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 76 | 77 | [[package]] 78 | name = "platformdirs" 79 | version = "2.5.2" 80 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 81 | category = "dev" 82 | optional = false 83 | python-versions = ">=3.7" 84 | 85 | [package.extras] 86 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] 87 | test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] 88 | 89 | [[package]] 90 | name = "python-dateutil" 91 | version = "2.8.2" 92 | description = "Extensions to the standard Python datetime module" 93 | category = "main" 94 | optional = false 95 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 96 | 97 | [package.dependencies] 98 | six = ">=1.5" 99 | 100 | [[package]] 101 | name = "six" 102 | version = "1.16.0" 103 | description = "Python 2 and 3 compatibility utilities" 104 | category = "main" 105 | optional = false 106 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 107 | 108 | [[package]] 109 | name = "tomli" 110 | version = "2.0.1" 111 | description = "A lil' TOML parser" 112 | category = "dev" 113 | optional = false 114 | python-versions = ">=3.7" 115 | 116 | [[package]] 117 | name = "typing-extensions" 118 | version = "4.3.0" 119 | description = "Backported and Experimental Type Hints for Python 3.7+" 120 | category = "dev" 121 | optional = false 122 | python-versions = ">=3.7" 123 | 124 | [[package]] 125 | name = "xmltodict" 126 | version = "0.13.0" 127 | description = "Makes working with XML feel like you are working with JSON" 128 | category = "main" 129 | optional = false 130 | python-versions = ">=3.4" 131 | 132 | [metadata] 133 | lock-version = "1.1" 134 | python-versions = "^3.8" 135 | content-hash = "d669eccb63f144a7c8e6db8da3e3eef13ca41e0925dab319f47f4bceb533a502" 136 | 137 | [metadata.files] 138 | black = [ 139 | {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, 140 | {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, 141 | {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, 142 | {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, 143 | {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, 144 | {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, 145 | {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, 146 | {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, 147 | {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, 148 | {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, 149 | {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, 150 | {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, 151 | {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, 152 | {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, 153 | {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, 154 | {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, 155 | {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, 156 | {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, 157 | {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, 158 | {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, 159 | {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, 160 | {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, 161 | {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, 162 | ] 163 | click = [ 164 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 165 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 166 | ] 167 | colorama = [ 168 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 169 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 170 | ] 171 | coverage = [ 172 | {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, 173 | {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, 174 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, 175 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, 176 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, 177 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, 178 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, 179 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, 180 | {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, 181 | {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, 182 | {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, 183 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, 184 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, 185 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, 186 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, 187 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, 188 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, 189 | {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, 190 | {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, 191 | {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, 192 | {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, 193 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, 194 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, 195 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, 196 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, 197 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, 198 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, 199 | {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, 200 | {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, 201 | {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, 202 | {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, 203 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, 204 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, 205 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, 206 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, 207 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, 208 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, 209 | {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, 210 | {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, 211 | {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, 212 | {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, 213 | ] 214 | ddt = [ 215 | {file = "ddt-1.5.0-py2.py3-none-any.whl", hash = "sha256:78976df724bbf70e8f8c488094184fd778ce3212deb0070481b4db44d5e0fb38"}, 216 | {file = "ddt-1.5.0.tar.gz", hash = "sha256:daad6bc5fc7619e5aa7eed6c394e05bf82946160a13e5df2e26de176cbaf347e"}, 217 | ] 218 | mypy-extensions = [ 219 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 220 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 221 | ] 222 | pathspec = [ 223 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 224 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 225 | ] 226 | platformdirs = [ 227 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 228 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 229 | ] 230 | python-dateutil = [ 231 | {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, 232 | {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, 233 | ] 234 | six = [ 235 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 236 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 237 | ] 238 | tomli = [ 239 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 240 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 241 | ] 242 | typing-extensions = [ 243 | {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, 244 | {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, 245 | ] 246 | xmltodict = [ 247 | {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, 248 | {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, 249 | ] 250 | --------------------------------------------------------------------------------