From 6f131d92384281ec42744863ab87d89b785a84c2 Mon Sep 17 00:00:00 2001 From: Roy Cheung <1164076+cheungr@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:49:04 -0700 Subject: [PATCH] Rewrite Withings to Garmin sync for Docker Replace legacy multi-target sync with a focused, persistent Docker Compose service and first-class Garmin MFA. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .dockerignore | 7 + .env.example | 10 +- .github/workflows/build-publish.yml | 97 -- .github/workflows/docker-image.yml | 6 - .github/workflows/release.yml | 44 + .github/workflows/test.yml | 17 + .gitignore | 3 +- Dockerfile | 53 +- MANIFEST.in | 1 - README.md | 656 +----------- contrib/icon.png | Bin 195554 -> 0 bytes contrib/k8s-bootstrap.yaml | 51 - contrib/k8s-deployment.yaml | 112 --- contrib/k8s-job.yaml | 48 - contrib/k8s-pvc.yaml | 18 - contrib/withings.html | 37 - docker-compose.yml | 24 + poetry.lock | 1280 ------------------------ pyproject.toml | 53 +- tests/test_fit.py | 55 + tests/test_garmin.py | 32 + tests/test_storage.py | 85 ++ tests/test_sync.py | 95 ++ tests/test_withings.py | 48 + withings_sync/config/withings_app.json | 5 - withings_sync/fit.py | 451 +++------ withings_sync/garmin.py | 247 +++-- withings_sync/storage.py | 63 ++ withings_sync/sync.py | 774 +++----------- withings_sync/trainerroad.py | 211 ---- withings_sync/withings.py | 278 +++++ withings_sync/withings2.py | 529 ---------- 32 files changed, 1291 insertions(+), 4099 deletions(-) create mode 100644 .dockerignore delete mode 100644 .github/workflows/build-publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml delete mode 100644 MANIFEST.in delete mode 100644 contrib/icon.png delete mode 100644 contrib/k8s-bootstrap.yaml delete mode 100644 contrib/k8s-deployment.yaml delete mode 100644 contrib/k8s-job.yaml delete mode 100644 contrib/k8s-pvc.yaml delete mode 100644 contrib/withings.html create mode 100644 docker-compose.yml delete mode 100644 poetry.lock create mode 100644 tests/test_fit.py create mode 100644 tests/test_garmin.py create mode 100644 tests/test_storage.py create mode 100644 tests/test_sync.py create mode 100644 tests/test_withings.py delete mode 100644 withings_sync/config/withings_app.json create mode 100644 withings_sync/storage.py delete mode 100644 withings_sync/trainerroad.py create mode 100644 withings_sync/withings.py delete mode 100644 withings_sync/withings2.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4f3c01d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.venv +data +.env +tests +__pycache__ +*.py[cod] diff --git a/.env.example b/.env.example index ff63920..ec249f4 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,8 @@ -GARMIN_USERNAME= -GARMIN_PASSWORD= +WITHINGS_CLIENT_ID= +WITHINGS_CLIENT_SECRET= +WITHINGS_REDIRECT_URI= +SYNC_INTERVAL=1h +TZ=UTC +LOG_LEVEL=INFO +PUID=1000 +PGID=1000 diff --git a/.github/workflows/build-publish.yml b/.github/workflows/build-publish.yml deleted file mode 100644 index b80a5c2..0000000 --- a/.github/workflows/build-publish.yml +++ /dev/null @@ -1,97 +0,0 @@ ---- - -name: Build and Publish Release to PyPI & GitHub - -on: push - -jobs: - build: - name: Build distribution 📦 - runs-on: ubuntu-latest - steps: - - name: Checkout repository 🛒 - uses: actions/checkout@v4 - - name: Update version in pyproject.toml ⬆️ - if: startsWith(github.ref, 'refs/tags/') - run: | - VERSION=${GITHUB_REF#refs/tags/} - sed -i "s/version = \"1.0.0.dev1\"/version = \"$VERSION\"/" pyproject.toml - - name: Set up Python 3.12 🐍 - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install and configure Poetry 📜 - uses: snok/install-poetry@v1 - with: - virtualenvs-create: false - - name: Build & Package project 👷 - run: poetry build - - name: Store the distribution packages - uses: actions/upload-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - publish-to-pypi: - name: Publish Python 🐍 distribution 📦 to PyPI - if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes - needs: - - build - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/withings-sync - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - name: Publish distribution 📦 to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} # LEAVING THIS HERE UNTIL TRUSTED PUBLISHING IS IN PLACE - - publish-to-github-release: - name: Upload the Python 🐍 distribution to GitHub Release - if: startsWith(github.ref, 'refs/tags/') # only upload to GitHub Release on tag pushes - needs: - - build - runs-on: ubuntu-latest - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - name: Release - uses: fnkr/github-action-ghr@v1 - env: - GHR_PATH: dist/ - GHR_REPLACE: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - publish-to-testpypi: - name: Publish Python 🐍 distribution 📦 to TestPyPI - needs: - - build - runs-on: ubuntu-latest - environment: - name: testpypi - url: https://test.pypi.org/p/withings-sync - permissions: - id-token: write # IMPORTANT: mandatory for trusted publishing - steps: - - name: Download all the dists - uses: actions/download-artifact@v4 - with: - name: python-package-distributions - path: dist/ - - name: Publish distribution 📦 to TestPyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://test.pypi.org/legacy/ - skip-existing: true diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 5badd11..752e62f 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -25,12 +25,6 @@ jobs: - name: Checkout repository 🛒 uses: actions/checkout@v6 - - name: Update version in pyproject.toml ⬆️ - if: startsWith(github.ref, 'refs/tags/') - run: | - VERSION=${GITHUB_REF#refs/tags/} - sed -i "s/version = \"1.0.0.dev1\"/version = \"$VERSION\"/" pyproject.toml - - name: Set up QEMU 👷 uses: docker/setup-qemu-action@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d1dd342 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,44 @@ +name: Publish Python package + +on: + push: + tags: + - "v*" + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Set version from tag + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + python - <<'PY' + import os + import re + from pathlib import Path + + version = os.environ["RELEASE_TAG"].removeprefix("v") + if not re.fullmatch(r"\d+\.\d+\.\d+(?:[a-zA-Z0-9.-]+)?", version): + raise SystemExit("Release tag must contain a valid package version.") + path = Path("pyproject.toml") + content, count = re.subn( + r'(?m)^version = "[^"]+"$', + f'version = "{version}"', + path.read_text(), + count=1, + ) + if count != 1: + raise SystemExit("Could not update the project version.") + path.write_text(content) + PY + - run: python -m pip install build + - run: python -m build + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..839b5c4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,17 @@ +name: Test and build + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - run: pip install -e ".[test]" + - run: pytest + - run: docker compose build diff --git a/.gitignore b/.gitignore index 79900a9..59d9eb0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ *.egg-info/ dist/ .idea -*.json .env +data/ +.venv/ diff --git a/Dockerfile b/Dockerfile index 3b459ab..44e4ebe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,22 @@ -FROM python:3.12-alpine +FROM python:3.12-slim -ARG PROJECT="withings-sync" -ARG PACKAGE="withings_sync" -ARG USER_UID=1000 -ARG USER_GID=$USER_UID +ARG PUID=1000 +ARG PGID=1000 -RUN apk --no-cache add supercronic +RUN apt-get update && apt-get install -y --no-install-recommends tzdata && \ + rm -rf /var/lib/apt/lists/* && \ + groupadd --gid "${PGID}" app && \ + useradd --uid "${PUID}" --gid app --create-home --shell /usr/sbin/nologin app && \ + mkdir -p /app /data && chown app:app /app /data -RUN if getent passwd ${USER_UID} >/dev/null; then \ - deluser $(getent passwd ${USER_UID} | cut -d: -f1); fi && \ - if getent group ${USER_GID} >/dev/null; then \ - delgroup $(getent group ${USER_GID} | cut -d: -f1); fi -RUN addgroup --system --gid ${USER_GID} ${PROJECT} && \ - adduser --system --disabled-password --home /home/${PROJECT} \ - --uid ${USER_UID} --ingroup ${PROJECT} ${PROJECT} +WORKDIR /app +COPY pyproject.toml README.md ./ +COPY withings_sync ./withings_sync +RUN pip install --no-cache-dir . -ENV PROJECT_DIR="/home/${PROJECT}" +ENV DATA_DIR=/data \ + PYTHONUNBUFFERED=1 -USER $PROJECT -WORKDIR $PROJECT_DIR - -ENV PATH="${PROJECT_DIR}/.poetry/bin:${PATH}" \ - PATH="${PROJECT_DIR}/.local/bin:${PATH}" \ - PIP_ROOT_USER_ACTION=ignore \ - PIP_DISABLE_PIP_VERSION_CHECK=on \ - POETRY_HOME="${PROJECT_DIR}/.poetry" \ - POETRY_NO_INTERACTION=1 \ - POETRY_VIRTUALENVS_IN_PROJECT=1 \ - POETRY_VIRTUALENVS_CREATE=1 \ - POETRY_CACHE_DIR="/tmp/poetry_cache" - -RUN pip install poetry - -COPY --chown=$PROJECT:$PROJECT pyproject.toml poetry.lock README.md $PROJECT_DIR/ -RUN poetry install --without dev --no-root && rm -rf $POETRY_CACHE_DIR - -COPY --chown=$PROJECT:$PROJECT $PACKAGE ./$PACKAGE/ -RUN poetry install --without dev - -ENTRYPOINT ["poetry", "run", "withings-sync"] +USER app +ENTRYPOINT ["withings-sync"] +CMD [] diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 695153d..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include withings_sync/config/*.json \ No newline at end of file diff --git a/README.md b/README.md index c20ec58..5c2b750 100644 --- a/README.md +++ b/README.md @@ -1,634 +1,58 @@ -> [!CAUTION] -> This Release introduces breaking changes that users need to be aware of before upgrading or using it. -> These changes were made to enhance security and compatibility but may require modifications to your existing setup. -> -> - The container now runs without root privileges. -> - Dependencies, virtual envs, packaging is now done by Poetry. -> - This fork requires a recent version of Python, currently capped at >= python 3.12. -> -> Make sure to go over the updated readme and test these new changes thoroughly for your environment. -> Chances are quite high you will have to make changes to make this work again. +# Withings → Garmin Connect -# withings-sync +Sync Withings scale weight and body composition (fat %, hydration %, muscle, bone, and BMI) to Garmin Connect. Runs as one non-root Docker Compose service; credentials and sync state live in `./data`. -A tool for synchronisation of the Withings API to: +## Ubuntu quickstart -- Garmin Connect -- Trainer Road -- raw JSON output +1. Install Docker Engine and the Docker Compose plugin using Docker's [Ubuntu installation guide](https://docs.docker.com/engine/install/ubuntu/). +2. Create a Withings developer application at [developer.withings.com](https://developer.withings.com/). Set its callback/redirect URL to the exact URL you will enter as `WITHINGS_REDIRECT_URI`, and enable the `user.metrics` scope. You need your app's client ID and client secret; do not use credentials from another app. +3. Prepare configuration and the writable data directory: -## 1. Installation Instructions -### 1.1 Installation of withings-sync with pip -> This method installs the package asuming you have a working python and pip installation. It relies on an external scheduler (e.g., cron on the host operating system) to manage job execution. -
- Expand to show installation steps. + ```sh + cp .env.example .env + sed -i "s/^PUID=.*/PUID=$(id -u)/; s/^PGID=.*/PGID=$(id -g)/" .env + chmod 600 .env + mkdir -p data + sudo chown "$(id -u):$(id -g)" data + ``` - 1. installing the package: + Set `WITHINGS_CLIENT_ID`, `WITHINGS_CLIENT_SECRET`, `WITHINGS_REDIRECT_URI` in `.env`. The default scheduler interval is `1h`; `SYNC_INTERVAL` also accepts seconds, minutes, and days (for example `30m`). If you change `PUID`/`PGID`, make `data/` writable by that UID/GID and rebuild. +4. Build the image and authorize Withings: - ```bash - $ pip install withings-sync - ``` + ```sh + docker compose build + docker compose run --rm withings-sync auth withings + ``` - 2. obtaining Withings authorization: + Open the printed URL, approve access, then paste the complete redirect URL (or just its `code`) into the prompt. The code is exchanged immediately. Withings codes are short-lived, so paste the redirect as soon as authorization completes. +5. Authenticate Garmin. The command prompts for your email/password and, when required, an MFA code. Passwords are used for this login only and are not saved: - When running for a very first time, you need to obtain Withings authorization: - ```bash - $ withings-sync - 2024-12-01 01:29:02,601 - withings - ERROR - Can\'t read config file /home/youruser/.withings_user.json - 2024-12-01 01:29:02,602 - withings - WARNING - User interaction needed to get Authentification Code from Withings! - 2024-12-01 01:29:02,603 - withings - WARNING - - 2024-12-01 01:29:02,603 - withings - WARNING - Open the following URL in your web browser and copy back the token. You will have *30 seconds* before the token expires. HURRY UP! - 2024-12-01 01:29:02,603 - withings - WARNING - (This is one-time activity) - 2024-12-01 01:29:02,604 - withings - WARNING - - 2024-12-01 01:29:02,604 - withings - INFO - https://account.withings.com/oauth2_user/authorize2?response_type=code&client_id=183e03e1f363110b3551f96765c98c10e8f1aa647a37067a1cb64bbbaf491626&state=OK&scope=user.metrics&redirect_uri=https://jaroslawhartman.github.io/withings-sync/contrib/withings.html& - 2024-12-01 01:29:02,604 - withings - INFO - + ```sh + docker compose run --rm withings-sync auth garmin + ``` - Token : + If you already have an authenticator code ready, it can be supplied with `--mfa-code CODE`; otherwise, enter it at the prompt. The saved Garmin token is reused and refreshed without asking for your password. +6. Start the hourly sync service and follow its logs: - 2024-12-01 01:31:07,832 - withings - INFO - Get Access Token - 2024-12-01 01:31:08,313 - withings - INFO - Refresh Access Token - 2024-12-01 01:31:08,771 - root - INFO - Fetching measurements from 2024-12-01 00:00 to 2024-12-01 23:59 - 2024-12-01 01:31:09,406 - withings - INFO - Get Measurements - 2024-12-01 01:31:09,856 - root - ERROR - No measurements to upload for date or period specified - ``` - You need to visit the URL listed by the script and then - copy Authentification Code back to the prompt. + ```sh + docker compose up -d + docker compose logs -f + ``` - This is one-time activity and it will not be needed to repeat. +## Commands - 3. running the application: - - Subsequent runs will use the saved access tokens in `~/.withings_user.json` - ```bash - $ withings-sync - 2024-12-01 01:37:41,500 - withings - INFO - Refresh Access Token - 2024-12-01 01:37:41,954 - root - INFO - Fetching measurements from 2024-12-01 00:00 to 2024-12-01 23:59 - 2024-12-01 01:37:42,563 - withings - INFO - Get Measurements - 2024-12-01 01:37:43,069 - root - ERROR - No measurements to upload for date or period specified - ``` -
- -### 1.2 Installation of withings-sync with docker compose (not using cron) -> This method follows a default approach of utilizing a single container to run one job at a time, then exiting upon completion. It relies on an external scheduler (e.g., cron on the host operating system) to manage job execution. -
- Expand to show installation steps. - - 1. create the following file/directory structure: - - ```bash - . # STACK_PATH - ./.env # .env file containing your variables - ./docker-compose.yml # docker-compose file - ./config/withings-sync/ # config directory for withings-sync (will be created automatically) - ``` - - 2. contents of an example `.env` file: - - ```bash - TZ=Europe/Kyiv - STACK_PATH=/home/your_user/your_stack_name - GARMIN_USERNAME="your.name@domain.ext" - GARMIN_PASSWORD="YourPasswordHere" - ``` - - 3. contents of an example `docker-compose.yml` file (simplified with --config-folder): - - ```yaml - services: - withings-sync: - image: ghcr.io/jaroslawhartman/withings-sync:latest - container_name: withings-sync - stdin_open: true # docker run -i - tty: true # docker run -t - environment: - - TZ=${TZ:?err} - - GARMIN_USERNAME=${GARMIN_USERNAME:?err} - - GARMIN_PASSWORD=${GARMIN_PASSWORD:?err} - volumes: - - /etc/localtime:/etc/localtime:ro - - ${STACK_PATH:?err}/config/withings-sync:/config - command: ["--config-folder", "/config"] - restart: unless-stopped - ``` - - **Note**: Using `--config-folder` simplifies the setup by: - - Storing both Withings and Garmin session files in one directory - - No need to mount individual files - - Easier backup and migration of configuration - - 4. obtaining Withings authorization: - - ```bash - $ docker compose pull - [+] Pulling 13/13 - ✔ withings-sync Pulled 56.0s - ✔ cb8611c9fe51 Pull complete 4.2s - ✔ 52e189a1282f Pull complete 6.4s - ✔ 95e68cb0cebc Pull complete 19.0s - ✔ c3ba8bc06a4d Pull complete 19.3s - ✔ fc2b9c85008a Pull complete 21.6s - ✔ 0376fca350d9 Pull complete 21.7s - ✔ 4f4fb700ef54 Pull complete 21.9s - ✔ c749d618f51d Pull complete 43.0s - ✔ 86d00088bd8d Pull complete 43.2s - ✔ 98dec7b84387 Pull complete 52.8s - ✔ 8825309bd8c2 Pull complete 53.1s - ✔ 7747652082d6 Pull complete 53.3s - ``` - - First start to ensure the script can start successfully: - - ```bash - $ docker compose run -it --remove-orphans --entrypoint "poetry run withings-sync --config-folder /config" withings-sync - [+] Creating 1/1 - ✔ Network stack_default Created 0.5s - 2024-12-01 01:29:02,601 - withings - ERROR - Can\'t read config file /home/youruser/.withings_user.json - 2024-12-01 01:29:02,602 - withings - WARNING - User interaction needed to get Authentification Code from Withings! - 2024-12-01 01:29:02,603 - withings - WARNING - - 2024-12-01 01:29:02,603 - withings - WARNING - Open the following URL in your web browser and copy back the token. You will have *30 seconds* before the token expires. HURRY UP! - 2024-12-01 01:29:02,603 - withings - WARNING - (This is one-time activity) - 2024-12-01 01:29:02,604 - withings - WARNING - - 2024-12-01 01:29:02,604 - withings - INFO - https://account.withings.com/oauth2_user/authorize2?response_type=code&client_id=183e03e1f363110b3551f96765c98c10e8f1aa647a37067a1cb64bbbaf491626&state=OK&scope=user.metrics&redirect_uri=https://jaroslawhartman.github.io/withings-sync/contrib/withings.html& - 2024-12-01 01:29:02,604 - withings - INFO - - - Token : - - 2024-12-01 01:31:07,832 - withings - INFO - Get Access Token - 2024-12-01 01:31:08,313 - withings - INFO - Refresh Access Token - 2024-12-01 01:31:08,771 - root - INFO - Fetching measurements from 2024-12-01 00:00 to 2024-12-01 23:59 - 2024-12-01 01:31:09,406 - withings - INFO - Get Measurements - 2024-12-01 01:31:09,856 - root - ERROR - No measurements to upload for date or period specified - ``` - You need to visit the URL listed by the script and then - copy Authentification Code back to the prompt. - - This is one-time activity and it will not be needed to repeat. - - 5. running the container: - - Subsequent runs will use the saved access tokens in `~/.withings_user.json` - - ```bash - $ docker compose run -it --remove-orphans withings-sync 0.5s - [+] Creating 1/1 - ✔ Container stack-withings-sync-run-3f24bc7ec7f9 Removed - 2024-12-01 01:37:41,500 - withings - INFO - Refresh Access Token - 2024-12-01 01:37:41,954 - root - INFO - Fetching measurements from 2024-12-01 00:00 to 2024-12-01 23:59 - 2024-12-01 01:37:42,563 - withings - INFO - Get Measurements - 2024-12-01 01:37:43,069 - root - ERROR - No measurements to upload for date or period specified - ``` - - 6. updating to a newer version: - - ```bash - $ docker compose pull - $ docker compose run -it --remove-orphans withings-sync - ``` - - This will ensure that both Withings and Garmin session files persist across container restarts in the `/config` directory. -
- -### 1.3 Installation of withings-sync with docker compose (using supercronic) -> This method leverages the included supercronic package for scheduling jobs directly within the container. This eliminates the need for an external scheduler, allowing the container to manage job execution independently. -
- Expand to show installation steps. - - 1. create the following file/directory structure: - - > Make sure to create the directories (`mkdir`) & files (`touch`) upfront or docker will create them as root. - ```bash - . # STACK_PATH - ./.env # .env file containing your variables - ./docker-compose.yml # docker-compose file - ./config/withings-sync/ # config directory for withings-sync (will be created automatically) - ./config/withings-sync/entrypoint.sh # entrypoint.sh file containing your cmd & arguments - ``` - - 2. contents of an example `.env` file: - - ```bash - TZ=Europe/Kyiv - STACK_PATH=/home/youruser/withings-sync - GARMIN_USERNAME="your.name@domain.ext" - GARMIN_PASSWORD="YourPasswordHere" - ``` - - 3. contents of an example `entrypoint.sh` file (updated for --config-folder): - - ```bash - #!/bin/sh - echo "$(( $RANDOM % 59 +0 )) */3 * * * * * poetry run withings-sync --config-folder /config --features BLOOD_PRESSURE" > /home/withings-sync/cronjob - supercronic -debug -passthrough-logs /home/withings-sync/cronjob - ``` - - 4. contents of an example `docker-compose.yml` file (simplified with --config-folder): - - ```yaml - services: - withings-sync: - image: ghcr.io/jaroslawhartman/withings-sync:latest - container_name: withings-sync - stdin_open: true # docker run -i - tty: true # docker run -t - environment: - - TZ=${TZ:?err} - - GARMIN_USERNAME=${GARMIN_USERNAME:?err} - - GARMIN_PASSWORD=${GARMIN_PASSWORD:?err} - volumes: - - /etc/localtime:/etc/localtime:ro - - ${STACK_PATH:?err}/config/withings-sync:/config - - ${STACK_PATH:?err}/config/withings-sync/entrypoint.sh:/home/withings-sync/entrypoint.sh - entrypoint: "sh /home/withings-sync/entrypoint.sh" - restart: unless-stopped - ``` - - **Note**: Using `--config-folder` simplifies the setup by storing both Withings and Garmin session files in the `/config` directory, eliminating the need for multiple volume mounts. - - 5. obtaining Withings authorization: - - ```bash - $ docker compose pull - [+] Pulling 13/13 - ✔ withings-sync Pulled 56.0s - ✔ cb8611c9fe51 Pull complete 4.2s - ✔ 52e189a1282f Pull complete 6.4s - ✔ 95e68cb0cebc Pull complete 19.0s - ✔ c3ba8bc06a4d Pull complete 19.3s - ✔ fc2b9c85008a Pull complete 21.6s - ✔ 0376fca350d9 Pull complete 21.7s - ✔ 4f4fb700ef54 Pull complete 21.9s - ✔ c749d618f51d Pull complete 43.0s - ✔ 86d00088bd8d Pull complete 43.2s - ✔ 98dec7b84387 Pull complete 52.8s - ✔ 8825309bd8c2 Pull complete 53.1s - ✔ 7747652082d6 Pull complete 53.3s - ``` - - First start to ensure the container can start successfully: - - ```bash - $ docker compose run -it --remove-orphans --entrypoint "poetry run withings-sync --config-folder /config" withings-sync - [+] Creating 1/1 - ✔ Network stack_default Created 0.5s - 2024-12-01 01:29:02,601 - withings - ERROR - Can\'t read config file /home/youruser/.withings_user.json - 2024-12-01 01:29:02,602 - withings - WARNING - User interaction needed to get Authentification Code from Withings! - 2024-12-01 01:29:02,603 - withings - WARNING - - 2024-12-01 01:29:02,603 - withings - WARNING - Open the following URL in your web browser and copy back the token. You will have *30 seconds* before the token expires. HURRY UP! - 2024-12-01 01:29:02,603 - withings - WARNING - (This is one-time activity) - 2024-12-01 01:29:02,604 - withings - WARNING - - 2024-12-01 01:29:02,604 - withings - INFO - https://account.withings.com/oauth2_user/authorize2?response_type=code&client_id=183e03e1f363110b3551f96765c98c10e8f1aa647a37067a1cb64bbbaf491626&state=OK&scope=user.metrics&redirect_uri=https://jaroslawhartman.github.io/withings-sync/contrib/withings.html& - 2024-12-01 01:29:02,604 - withings - INFO - - - Token : - - 2024-12-01 01:31:07,832 - withings - INFO - Get Access Token - 2024-12-01 01:31:08,313 - withings - INFO - Refresh Access Token - 2024-12-01 01:31:08,771 - root - INFO - Fetching measurements from 2024-12-01 00:00 to 2024-12-01 23:59 - 2024-12-01 01:31:09,406 - withings - INFO - Get Measurements - 2024-12-01 01:31:09,856 - root - ERROR - No measurements to upload for date or period specified - ``` - - 6. running the container: - - And for subsequent runs we start docker compose and let the container run in the background. - Subsequent runs will use the saved access tokens in `~/.withings_user.json` - - ```bash - $ docker compose up -d --remove-orphans - [+] Running 1/1 - ✔ Container withings-sync Started 1.5s - ``` - - 7. logging: - - ```bash - $ docker compose logs withings-sync - withings-sync | WARN[2024-12-24T09:23:55+01:00] process reaping disabled, not pid 1 - withings-sync | INFO[2024-12-24T09:23:55+01:00] read crontab: /home/withings-sync/cronjob - withings-sync | DEBU[2024-12-24T09:23:55+01:00] try parse (7 fields): '53 */3 * * * poetry run' - withings-sync | DEBU[2024-12-24T09:23:55+01:00] failed to parse (7 fields): '53 */3 * * * poetry run': failed: syntax error in day-of-week field: 'poetry' - withings-sync | DEBU[2024-12-24T09:23:55+01:00] try parse (6 fields): '53 */3 * * * poetry' - withings-sync | DEBU[2024-12-24T09:23:55+01:00] failed to parse (6 fields): '53 */3 * * * poetry': failed: syntax error in year field: 'poetry' - withings-sync | DEBU[2024-12-24T09:23:55+01:00] try parse (5 fields): '53 */3 * * *' - withings-sync | DEBU[2024-12-24T09:23:55+01:00] job will run next at 2024-12-24 09:53:00 +0100 CET job.command="poetry run withings-sync --features BLOOD_PRESSURE" job.position=0 job.schedule="53 */3 * * *" - withings-sync | INFO[2024-12-24T09:53:00+01:00] starting iteration=0 job.command="poetry run withings-sync --features BLOOD_PRESSURE" job.position=0 job.schedule="53 */3 * * *" - withings-sync | 2024-12-24 09:53:29,177 - withings - INFO - Refresh Access Token - withings-sync | 2024-12-24 09:53:29,380 - root - INFO - Fetching measurements from 2024-12-22 18:52 to 2024-12-24 23:59 - withings-sync | 2024-12-24 09:53:29,662 - withings - INFO - Get Measurements - withings-sync | 2024-12-24 09:53:29,866 - root - INFO - 2024-12-24 08:08:57 This Withings metric contains no weight data or blood pressure. Not syncing... - withings-sync | 2024-12-24 09:53:29,868 - root - INFO - 2024-12-24 08:08:57 This Withings metric contains no weight data or blood pressure. Not syncing... - withings-sync | 2024-12-24 09:53:29,870 - root - INFO - 2024-12-24 08:08:57 This Withings metric contains no weight data or blood pressure. Not syncing... - withings-sync | 2024-12-24 09:53:29,878 - root - INFO - No blood pressure data to sync for FIT file - withings-sync | 2024-12-24 09:53:29,880 - root - INFO - No TrainerRoad username or a new measurement - skipping sync - withings-sync | 2024-12-24 09:53:33,665 - root - INFO - Fit file with weight information uploaded to Garmin Connect - withings-sync | 2024-12-24 09:53:33,666 - withings - INFO - Saving Last Sync - withings-sync | INFO[2024-12-24T09:53:34+01:00] job succeeded iteration=0 job.command="poetry run withings-sync --features BLOOD_PRESSURE" job.position=0 job.schedule="53 */3 * * *" - withings-sync | DEBU[2024-12-24T09:53:34+01:00] job will run next at 2024-12-24 12:53:00 +0100 CET job.command="poetry run withings-sync --features BLOOD_PRESSURE" job.position=0 job.schedule="53 */3 * * *" - ``` - - 8. updating to a newer version: - - ```bash - $ docker compose pull - $ docker compose down - $ docker compose up -d --remove-orphans - $ docker image prune -f - ``` - - This will ensure that both Withings and Garmin session files persist across container restarts in the `/config` directory. -
- -### 1.4 Installation of withings-sync with docker (not compose) -> This method follows a default approach of utilizing a single container to run one job at a time, then exiting upon completion. It relies on an external scheduler (e.g., cron on the host operating system) to manage job execution. -
- Expand to show installation steps. - -```bash -$ docker pull ghcr.io/jaroslawhartman/withings-sync:latest +```sh +docker compose run --rm withings-sync status +docker compose run --rm withings-sync sync --once +docker compose run --rm withings-sync sync --since 2026-01-01 ``` -First start to ensure the script can start successfully (using --config-folder for simplified session management): - -Obtaining Withings authorisation: - -```bash -# Create a local config directory -mkdir -p ./config - -# Run with config folder mounted -$ docker run -v ./config:/config --interactive --tty --name withings-sync ghcr.io/jaroslawhartman/withings-sync:latest --config-folder /config --garmin-username= --garmin-password= - -Can't read config file /config/.withings_user.json -User interaction needed to get Authentification Code from Withings! - -Open the following URL in your web browser and copy back the token. You will have *30 seconds* before the token expires. HURRY UP! -(This is one-time activity) +`sync --since` performs a one-time backfill starting on the given UTC date. Sync state records successfully uploaded Withings measurement IDs, so reruns skip them. The service automatically retries at the next interval after a scheduled failure. -https://account.withings.com/oauth2_user/authorize2?response_type=code&client_id=183e03e1f363110b3551f96765c98c10e8f1aa647a37067a1cb64bbbaf491626&state=OK&scope=user.metrics&redirect_uri=https://jaroslawhartman.github.io/withings-sync/contrib/withings.html& +If Garmin credentials truly expire or are revoked, logs report the re-auth command: -Token : -Withings: Get Access Token -Withings: Refresh Access Token -Withings: Get Measurements - Measurements received -JaHa.WAW.PL -Garmin Connect User Name: JaHa.WAW.PL -Fit file uploaded to Garmin Connect +```sh +docker compose run --rm withings-sync auth garmin ``` -And for subsequent runs (both session files persist in ./config): - -```bash -$ docker start -i withings-sync -Withings: Refresh Access Token -Withings: Get Measurements - Measurements received -JaHa.WAW.PL -Garmin Connect User Name: JaHa.WAW.PL -Fit file uploaded to Garmin Connect -``` - -**Note**: Using `--config-folder /config` stores both Withings and Garmin session files in the mounted directory, making them persist across container restarts. -
- -## 2. Usage Instructions - -``` -usage: withings-sync [-h] [--version] [--garmin-username GARMIN_USERNAME] [--garmin-password GARMIN_PASSWORD] [--trainerroad-username TRAINERROAD_USERNAME] [--trainerroad-password TRAINERROAD_PASSWORD] [--fromdate DATE] [--todate DATE] [--to-fit] [--to-json] - [--output BASENAME] [--no-upload] [--features BLOOD_PRESSURE [BLOOD_PRESSURE ...]] [--verbose | --silent] [--dump-raw] [--config-folder PATH] - -A tool for synchronisation of Withings (ex. Nokia Health Body) to Garmin Connect and Trainer Road or to provide a json string. - -options: - -h, --help show this help message and exit - --version, -V show program's version number and exit - --garmin-username GARMIN_USERNAME, --gu GARMIN_USERNAME - Username to log in to Garmin Connect. - --garmin-password GARMIN_PASSWORD, --gp GARMIN_PASSWORD - Password to log in to Garmin Connect. - --trainerroad-username TRAINERROAD_USERNAME, --tu TRAINERROAD_USERNAME - Username to log in to TrainerRoad. - --trainerroad-password TRAINERROAD_PASSWORD, --tp TRAINERROAD_PASSWORD - Password to log in to TrainerRoad. - --fromdate DATE, -f DATE - Date to start syncing from. Ex: 2023-12-20 - --todate DATE, -t DATE - Date for the last sync. Ex: 2023-12-30 - --to-fit, -F Write output file in FIT format. - --to-json, -J Write output file in JSON format. - --output BASENAME, -o BASENAME - Write downloaded measurements to file. - --no-upload Won't upload to Garmin Connect or TrainerRoad. - --features BLOOD_PRESSURE [BLOOD_PRESSURE ...] - Enable Features like BLOOD_PRESSURE. - --verbose, -v Run verbosely. - --silent, -s Run silently (suppress INFO messages). - --dump-raw, -R Dump the raw Withings API JSON for the selected date range to file. If --output is provided, the file will be BASENAME.withings_raw.json. Otherwise, a default filename with the date range will be used. - --config-folder PATH, -c PATH - Path to config folder for session files (if not specified, uses legacy paths in home directory). -``` - -## 3. Providing credentials -### 3.1 Providing credentials via environment variables - -You can use the following environment variables for providing the Garmin and/or Trainerroad credentials: - -- `GARMIN_USERNAME` -- `GARMIN_PASSWORD` -- `TRAINERROAD_USERNAME` -- `TRAINERROAD_PASSWORD` - -The CLI also uses python-dotenv to populate the variables above. Therefore setting the environment variables -has the same effect as placing the variables in a `.env` file in the working directory. - -### 3.2 Providing credentials via secrets files - -You can also populate the following 'secrets' files to provide the Garmin and/or Trainerroad credentials: - -- `/run/secrets/garmin_username` -- `/run/secrets/garmin_password` -- `/run/secrets/trainerroad_username` -- `/run/secrets/trainerroad_password` - -Secrets are useful in an orchestrated container context — see the [Docker Swarm](https://docs.docker.com/engine/swarm/secrets/) or [Rancher](https://rancher.com/docs/rancher/v1.6/en/cattle/secrets/) docs for more information on how to securely inject secrets into a container. - -### 3.3 Order of priority for credentials - -In the case of credentials being available via multiple means (e.g. [environment variables](#providing-credentials-via-environment-variables) and [secrets files](#providing-credentials-via-secrets-files)), the order of resolution for determining which credentials to use is as follows, with later methods overriding credentials supplied by an earlier method: - -1. Read secrets file(s) -2. Read environment variable(s), variables set explicitly take precedence over values from a `.env` file. -3. Use command invocation argument(s) - -## 4. Tips - -### 4.1 Garmin SSO errors - -Some users reported errors raised by the Garmin SSO login: - -``` -withings_sync.garmin.APIException: SSO error 401 -``` - -or - -``` -withings_sync.garmin.APIException: SSO error 403 -``` - -These errors are raised if a user tries to login too frequently. -E.g. by running the script every 10 minutes. - -**We recommend to run the script around 8-10 times per day (every 2-3 hours).** - -See also: https://github.com/jaroslawhartman/withings-sync/issues/31 - -### 4.2 Garmin auth - -You can configure the location of the garmin session file with the variabe `GARMIN_SESSION`. - -Note: If you specify both `--config-folder` and the `GARMIN_SESSION` environment variable, the `--config-folder` option takes precedence. - -### 4.3 Config folder - -By default, withings-sync stores session files in your home directory: -- `~/.withings_user.json` for Withings authentication -- `~/.garmin_session` for Garmin authentication - -You can use the `--config-folder` or `-c` argument to store all session files in a custom folder: - -```bash -# Store session files in ~/.withings-sync -withings-sync --config-folder ~/.withings-sync - -# Store session files in a custom location -withings-sync --config-folder /path/to/my/config -``` - -When using a custom config folder: -- The folder will be created automatically if it doesn't exist -- If you have existing session files in the legacy locations, the script will show you where to copy them from -- This is useful for Docker installations or for keeping your home directory clean - -### 4.4 Run a periodic Kubernetes job - -1. Create the secret using kubectl in the command line. -``` -export GARMIN_USERNAME="user@username.com" -export GARMIN_PASSWORD="superPassword" -export TRAINERROAD_USERNAME: "name@name.com" -export TRAINERROAD_PASSWORD: "superPassword" -kubectl create secret generic withings-secret --from-literal=GARMIN_USERNAME=$GARMIN_USERNAME --from-literal=GARMIN_PASSWORD=$GARMIN_PASSWORD ---from-literal=TRAINERROAD_USERNAME=$TRAINERROAD_USERNAME --from-literal=TRAINERROAD_PASSWORD=$TRAINERROAD_PASSWORD -``` - -2. Create the PVC. -``` -kubectl apply -f k8s-pvc.yaml -``` - -3. Run the bootstrap pod, which attaches to the PVC for storing credentials. -``` -kubectl apply -f k8s-bootstrap.yaml -``` -The bootstrap pod stays on indefinitely to allow time for you to exec in, and -generate the credentials. - -4. Exec into the bootstrap pod, generate credentials and store them in the PVC. -``` -kubectl exec -it bootstrap-withings-sync -- sh -``` -From _within_ the bootstrap pod: -``` -# Create a config directory in the PVC -mkdir -p /data/config - -# Run with config folder to store all session files -poetry run withings-sync --config-folder /data/config --fromdate= -``` -It is important that this run includes a date that has a record, as a record is required for the program to attempt an upload to garmin in order to create the session files for garmin. -The command above will allow entering the withings token and the MFA code for garmin. -After successful auth, the credentials will be automatically stored in `/data/config/`: -- `/data/config/.withings_user.json` -- `/data/config/.garmin_session` - -5. Create the cron job. Update the cron job command to use the config folder: -``` -kubectl apply -f k8s-job.yaml -``` - -Note: Update your k8s-job.yaml to include `--config-folder /data/config` in the container command. - -## 5 For advanced users - registering own Withings application -> Instead of using the provided Withings application tokens you can register your own app with Withings and use that one instead. -
- If you are not sure you need this, you most likely won't. - -The script has been registered as a Withings application and got assigned `Client ID` and `Consumer Secret`. If you wish to create your own application - feel free! - - -* First you need a Withings account. [Sign up here](https://account.withings.com/connectionuser/account_create). -* Then you need a Withings developer app registered. [Create your app here](https://account.withings.com/partner/add_oauth2). - -Note, registering it is quite cumbersome, as you need to have a callback URL and an Icon. Anyway, when done, you should have the following identifiers: - -| Identfier | Example | -|-----------------|--------------------------------------------------------------------| -| Client ID | `183e03.................765c98c10e8f1aa647a37067a1......baf491626` | -| Consumer Secret | `a75d65.................4c16719ef7bd69fa7c5d3fd0ea......ed48f1765` | -| Callback URI | `https://jhartman.pl/withings/notify` | - -Configure them in `config/withings_app.json`, for example: - -``` -{ - "callback_url": "https://wieloryb.uk.to/withings/withings.html", - "client_id": "183e0******0b3551f96765c98c1******b64bbbaf491626", - "consumer_secret": "a75d65******1df1514c16719ef7bd69fa7*****2e2b0ed48f1765" -} -``` - -For the callback URL you will need to setup a webserver hosting `contrib/withings.html`. - -To do this in a Docker installation, you can use the environment variable `WITHINGS_APP` to point to a mounted `withings_app.json` - -Example docker-compose: -``` - withings-sync: - container_name: withings-sync - image: ghcr.io/jaroslawhartman/withings-sync:latest - volumes: - - "withings-sync:/root" - - "/etc/localtime:/etc/localtime:ro" - environment: - WITHINGS_APP: /home/withings-sync/withings_app.json -(...) -``` -You can then add the app-config in `withings-sync/withings_app.json` - -
- -## 6. Release - -Release works via the GitHub [Draft a new Release](https://github.com/jaroslawhartman/withings-sync/releases/new) -function. -The `version` key in `pyproject.toml` will be bumped automatically (Version will be written to pyproject.toml file). - -### Docker Image - -Container images are created automagically by GitHub Action and published -to [ghcr](https://github.com/jaroslawhartman/withings-sync/pkgs/container/withings-sync). - -### Pypi & GitHub - -Will be conducted automatically within the Github-Release cycle. -This needs the permission on the [pypi-project](https://pypi.org/project/withings-sync/). -The python packages are added to the GitHub releases by a GitHub Action. - -## 7. References - -* SSO authorization derived from https://github.com/cpfair/tapiriik -* TrainerRoad API from https://github.com/stuwilkins/python-trainerroad - -## 8. Credits / Authors - -* Based on [withings-garmin](https://github.com/ikasamah/withings-garmin) by Masayuki Hamasaki, improved to support SSO authorization in Garmin Connect 2. -* Based on [withings-garmin-v2](https://github.com/jaroslawhartman/withings-garmin-v2) by Jarek Hartman, improved Python 3 compatability, code-style and setuptools packaging, Kubernetes and Docker support. +For Withings revocation or a missing refresh token, repeat `auth withings`. Keep `./data` private and backed up; it contains long-lived account tokens. `./data` is the only persistent volume. diff --git a/contrib/icon.png b/contrib/icon.png deleted file mode 100644 index c9eb8b35b593c5dc0d60f63c335733332e98983a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 195554 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

5*(EiMS694t<*MH-aTVTz>q#Jy03s=dHz0DgTsb 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 WvVw*NZo^23Kk#EitzwN=;r|Z_B(v`T diff --git a/contrib/k8s-bootstrap.yaml b/contrib/k8s-bootstrap.yaml deleted file mode 100644 index ebe3442..0000000 --- a/contrib/k8s-bootstrap.yaml +++ /dev/null @@ -1,51 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: bootstrap-withings-sync - labels: - app.kubernetes.io/name: bootstrap-withings-sync -spec: - restartPolicy: Never - securityContext: - fsGroup: 1000 - initContainers: - - name: prep-pvc - image: busybox:1.36 - command: - - sh - - -lc - - | - set -eux - chown -R 1000:1000 /data - chmod 755 /data - ls -la /data - - volumeMounts: - - name: creds - mountPath: /data - - containers: - - name: bootstrap-withings-sync - image: ghcr.io/jaroslawhartman/withings-sync:latest - command: ["sh","-lc","sleep infinity"] - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - envFrom: - - secretRef: - name: withings-secret - resources: - requests: - cpu: "200m" - memory: "512Mi" - limits: - cpu: "200m" - memory: "512Mi" - volumeMounts: - - name: creds - mountPath: /data - - volumes: - - name: creds - persistentVolumeClaim: - claimName: withings-oauth-cache diff --git a/contrib/k8s-deployment.yaml b/contrib/k8s-deployment.yaml deleted file mode 100644 index e0c38c9..0000000 --- a/contrib/k8s-deployment.yaml +++ /dev/null @@ -1,112 +0,0 @@ -# Deploy withings-sync as a Kubernetes deployment -# Please also see contrib/k8s-job.yaml for a Kubernetes CronJob -# -# To install: -# - kubectl create namespace FOO -# - kubectl -n FOO create -f (this file) -# -# To check logs: -# - kubectl -n FOO logs -l app=withings-sync -# -# Because the app is running without STDIN, it will not pause to wait -# for you to enter the oauth token. Instead it will throw an error -# EOFError: EOF when reading a line -# and continue to sleep. To set up for the first time, run something -# like this manually, and enter the oauth token provided after you -# follow the link given: -# -# - kubectl -n FOO exec -ti $( kubectl -n FOO get pods | awk '$3=="Running" {print $1; exit}' ) -- withings-sync - - ---- -# Secret for credentials. By defining this as stringData: -# instead of data: we can enter details here in plain text and -# it gets base64 encoded when added to the system. -# -# Leave unused variables defined, but empty "" -# -apiVersion: v1 -kind: Secret -metadata: - name: credentials -type: Opaque -stringData: - # NOT base64 - GARMIN_USERNAME: "someone@example.com" - GARMIN_PASSWORD: "password-goes-here" - TRAINERROAD_PASSWORD: "" - TRAINERROAD_USERNAME: "" - ---- -# PVC for storing Withings OAuth tokens. Different -# storage methods have different minimum sizes. -# -# If you don't have a default storage class, you must -# specify it -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: withings-oauth-cache -spec: - accessModes: - - ReadWriteOnce - # uncomment, and define if needed - # storageClassName: class - resources: - requests: - storage: 1Mi - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: withings-sync -spec: - replicas: 1 - revisionHistoryLimit: 3 - selector: - matchLabels: - app: withings-sync - template: - metadata: - labels: - app: withings-sync - spec: - containers: - - name: withings-sync - image: ghcr.io/jaroslawhartman/withings-sync:master - imagePullPolicy: IfNotPresent - - # override $HOME to put our oauth file in a known place - env: - - name: HOME - value: /data - - # read usernames/passwords from the secret we defined - envFrom: - - secretRef: - name: credentials - - volumeMounts: - - mountPath: /data - name: oauth-cache - - # run every 15 minutes, synched to clock time - command: - - sh - - -c - - while true; do date; withings-sync; echo; sleep 10000; done - - # Never run this container as root - securityContext: - fsGroup: 1234 - runAsGroup: 1234 - runAsUser: 1234 - runAsNonRoot: True - - volumes: - - name: oauth-cache - persistentVolumeClaim: - claimName: withings-oauth-cache - - diff --git a/contrib/k8s-job.yaml b/contrib/k8s-job.yaml deleted file mode 100644 index c914fc4..0000000 --- a/contrib/k8s-job.yaml +++ /dev/null @@ -1,48 +0,0 @@ -apiVersion: batch/v1 -kind: CronJob -metadata: - name: withings-garmin-sync -spec: - schedule: '0 */3 * * *' - timeZone: "Europe/London" # NeedS k8s >= 1.27. - successfulJobsHistoryLimit: 3 - concurrencyPolicy: Allow - failedJobsHistoryLimit: 1 - jobTemplate: - spec: - template: - spec: - restartPolicy: Never - containers: - - name: withings-garmin-sync - image: ghcr.io/jaroslawhartman/withings-sync:latest - imagePullPolicy: IfNotPresent - - volumeMounts: - - mountPath: /data - name: oauth-cache - - # Run the container as user 1000 'withings-sync' - securityContext: - runAsGroup: 1000 - runAsUser: 1000 - - command: - - sh - - -c - - | - set -eux - ln -sf /data/.withings_user.json /home/withings-sync/.withings_user.json - ln -sf /data/.garmin_session /home/withings-sync/.garmin_session - ls -la /home/withings-sync/ - poetry run withings-sync --features BLOOD_PRESSURE - - envFrom: - - secretRef: - name: withings-secret - - volumes: - - name: oauth-cache - persistentVolumeClaim: - claimName: withings-oauth-cache - diff --git a/contrib/k8s-pvc.yaml b/contrib/k8s-pvc.yaml deleted file mode 100644 index 777cabb..0000000 --- a/contrib/k8s-pvc.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# PVC for storing Withings OAuth tokens. Different -# storage methods have different minimum sizes. -# -# If you don't have a default storage class, you must -# specify it -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: withings-oauth-cache -spec: - accessModes: - - ReadWriteOnce - # uncomment, and define if needed - # storageClassName: class - resources: - requests: - storage: 1Mi - volumeMode: Filesystem diff --git a/contrib/withings.html b/contrib/withings.html deleted file mode 100644 index c9cd5f3..0000000 --- a/contrib/withings.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - Withings Authorization Token - - -

Your Withings Authorization Token

- - -

You must copy it back to the script during next 30 seconds, otherwise it will get expired.

- - - - diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..752ed9a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +services: + withings-sync: + build: + context: . + args: + PUID: ${PUID:-1000} + PGID: ${PGID:-1000} + restart: unless-stopped + env_file: + - path: .env + required: false + environment: + DATA_DIR: /data + SYNC_INTERVAL: ${SYNC_INTERVAL:-1h} + TZ: ${TZ:-UTC} + LOG_LEVEL: ${LOG_LEVEL:-INFO} + volumes: + - ./data:/data + healthcheck: + test: ["CMD", "withings-sync", "status", "--healthcheck"] + interval: 5m + timeout: 15s + start_period: 1m + retries: 3 diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 070e105..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1280 +0,0 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "astroid" -version = "3.3.6" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "astroid-3.3.6-py3-none-any.whl", hash = "sha256:db676dc4f3ae6bfe31cda227dc60e03438378d7a896aec57422c95634e8d722f"}, - {file = "astroid-3.3.6.tar.gz", hash = "sha256:6aaea045f938c735ead292204afdb977a36e989522b7833ef6fea94de743f442"}, -] - -[[package]] -name = "black" -version = "24.10.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, - {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, - {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, - {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, - {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, - {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, - {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, - {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, - {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, - {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, - {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, - {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, - {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, - {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, - {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, - {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, - {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, - {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, - {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, - {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, - {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, - {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2024.12.14" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -groups = ["main"] -files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "dill" -version = "0.3.9" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, - {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - -[[package]] -name = "executing" -version = "2.2.1" -description = "Get the currently executing AST node of a frame, and other information" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"}, - {file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] - -[[package]] -name = "garth" -version = "0.7.11" -description = "Garmin SSO auth + Connect client" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "garth-0.7.11-py3-none-any.whl", hash = "sha256:ce121fe2479e396968cb59de734ca3ff2edd2eac5aa88a36f30c4e906be0a105"}, - {file = "garth-0.7.11.tar.gz", hash = "sha256:c41f5c75b2a9a68f942c4c286ad6b7440f70db2d172c4d494add0cfcf3dad67f"}, -] - -[package.dependencies] -logfire = ">=2.11,<5.0" -pydantic = ">=2.0.0,<3.0.0" -pydantic-settings = ">=2.0.0,<3.0.0" -requests = ">=2.0.0,<3.0.0" -requests-oauthlib = ">=1.3.1,<3.0.0" - -[package.extras] -docs = ["zensical"] - -[[package]] -name = "googleapis-common-protos" -version = "1.73.0" -description = "Common protobufs used in Google APIs" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8"}, - {file = "googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a"}, -] - -[package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" - -[package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0)"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] - -[[package]] -name = "importlib-resources" -version = "6.4.5" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, - {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] -type = ["pytest-mypy"] - -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - -[[package]] -name = "logfire" -version = "4.29.0" -description = "The best Python observability tool! 🪵🔥" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "logfire-4.29.0-py3-none-any.whl", hash = "sha256:8dd7fdf6bed21459b8893eaa290d61977b9ebcc901844e365ddee868b5d8bca8"}, - {file = "logfire-4.29.0.tar.gz", hash = "sha256:18a306a0b5744aee8ad0a8f5d6b3a47a6d8951c340eaecc42dc5d0224f4bdca0"}, -] - -[package.dependencies] -executing = ">=2.0.1" -opentelemetry-exporter-otlp-proto-http = ">=1.39.0,<1.40.0" -opentelemetry-instrumentation = ">=0.41b0" -opentelemetry-sdk = ">=1.39.0,<1.40.0" -protobuf = ">=4.23.4" -rich = ">=13.4.2" -typing-extensions = ">=4.1.0" - -[package.extras] -aiohttp = ["opentelemetry-instrumentation-aiohttp-client (>=0.42b0)"] -aiohttp-client = ["opentelemetry-instrumentation-aiohttp-client (>=0.42b0)"] -aiohttp-server = ["opentelemetry-instrumentation-aiohttp-server (>=0.55b0)"] -asgi = ["opentelemetry-instrumentation-asgi (>=0.42b0)"] -asyncpg = ["opentelemetry-instrumentation-asyncpg (>=0.42b0)"] -aws-lambda = ["opentelemetry-instrumentation-aws-lambda (>=0.42b0)"] -celery = ["opentelemetry-instrumentation-celery (>=0.42b0)"] -datasets = ["httpx (>=0.27.2)", "pydantic (>=2)", "pydantic-evals (>=1.0.0) ; python_version >= \"3.10\""] -django = ["opentelemetry-instrumentation-asgi (>=0.42b0)", "opentelemetry-instrumentation-django (>=0.42b0)"] -dspy = ["openinference-instrumentation-dspy (>=0)"] -fastapi = ["opentelemetry-instrumentation-fastapi (>=0.42b0)"] -flask = ["opentelemetry-instrumentation-flask (>=0.42b0)"] -google-genai = ["opentelemetry-instrumentation-google-genai (>=0.4b0)"] -httpx = ["opentelemetry-instrumentation-httpx (>=0.42b0)"] -litellm = ["openinference-instrumentation-litellm (>=0)"] -mysql = ["opentelemetry-instrumentation-mysql (>=0.42b0)"] -psycopg = ["opentelemetry-instrumentation-psycopg (>=0.42b0)", "packaging"] -psycopg2 = ["opentelemetry-instrumentation-psycopg2 (>=0.42b0)", "packaging"] -pymongo = ["opentelemetry-instrumentation-pymongo (>=0.42b0)"] -redis = ["opentelemetry-instrumentation-redis (>=0.42b0)"] -requests = ["opentelemetry-instrumentation-requests (>=0.42b0)"] -sqlalchemy = ["opentelemetry-instrumentation-sqlalchemy (>=0.42b0)"] -sqlite3 = ["opentelemetry-instrumentation-sqlite3 (>=0.42b0)"] -starlette = ["opentelemetry-instrumentation-starlette (>=0.42b0)"] -system-metrics = ["opentelemetry-instrumentation-system-metrics (>=0.42b0)"] -variables = ["pydantic (>=2)"] -wsgi = ["opentelemetry-instrumentation-wsgi (>=0.42b0)"] - -[[package]] -name = "lxml" -version = "5.3.0" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, - {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, - {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, - {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, - {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, - {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, - {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, - {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, - {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, - {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, - {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, - {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, - {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, - {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, - {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, - {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, - {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, - {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, - {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, - {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, - {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, - {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml-html-clean"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.11)"] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, - {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins (>=0.5.0)"] -profiling = ["gprof2dot"] -rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "oauthlib" -version = "3.2.2" -description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, - {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, -] - -[package.extras] -rsa = ["cryptography (>=3.0.0)"] -signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] - -[[package]] -name = "opentelemetry-api" -version = "1.39.1" -description = "OpenTelemetry Python API" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, - {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, -] - -[package.dependencies] -importlib-metadata = ">=6.0,<8.8.0" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.39.1" -description = "OpenTelemetry Protobuf encoding" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464"}, -] - -[package.dependencies] -opentelemetry-proto = "1.39.1" - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.39.1" -description = "OpenTelemetry Collector Protobuf over HTTP Exporter" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985"}, - {file = "opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb"}, -] - -[package.dependencies] -googleapis-common-protos = ">=1.52,<2.0" -opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.39.1" -opentelemetry-proto = "1.39.1" -opentelemetry-sdk = ">=1.39.1,<1.40.0" -requests = ">=2.7,<3.0" -typing-extensions = ">=4.5.0" - -[package.extras] -gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.60b1" -description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d"}, - {file = "opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a"}, -] - -[package.dependencies] -opentelemetry-api = ">=1.4,<2.0" -opentelemetry-semantic-conventions = "0.60b1" -packaging = ">=18.0" -wrapt = ">=1.0.0,<2.0.0" - -[[package]] -name = "opentelemetry-proto" -version = "1.39.1" -description = "OpenTelemetry Python Proto" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007"}, - {file = "opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8"}, -] - -[package.dependencies] -protobuf = ">=5.0,<7.0" - -[[package]] -name = "opentelemetry-sdk" -version = "1.39.1" -description = "OpenTelemetry Python SDK" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c"}, - {file = "opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6"}, -] - -[package.dependencies] -opentelemetry-api = "1.39.1" -opentelemetry-semantic-conventions = "0.60b1" -typing-extensions = ">=4.5.0" - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.60b1" -description = "OpenTelemetry Semantic Conventions" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb"}, - {file = "opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953"}, -] - -[package.dependencies] -opentelemetry-api = "1.39.1" -typing-extensions = ">=4.5.0" - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] - -[[package]] -name = "protobuf" -version = "6.33.6" -description = "" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3"}, - {file = "protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326"}, - {file = "protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a"}, - {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2"}, - {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3"}, - {file = "protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593"}, - {file = "protobuf-6.33.6-cp39-cp39-win32.whl", hash = "sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e"}, - {file = "protobuf-6.33.6-cp39-cp39-win_amd64.whl", hash = "sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf"}, - {file = "protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901"}, - {file = "protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135"}, -] - -[[package]] -name = "pydantic" -version = "2.10.4" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, - {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" -typing-extensions = ">=4.12.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pydantic-settings" -version = "2.13.1" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237"}, - {file = "pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025"}, -] - -[package.dependencies] -pydantic = ">=2.7.0" -python-dotenv = ">=0.21.0" -typing-inspection = ">=0.4.0" - -[package.extras] -aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] -azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] -gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pylint" -version = "3.3.2" -description = "python code static checker" -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "pylint-3.3.2-py3-none-any.whl", hash = "sha256:77f068c287d49b8683cd7c6e624243c74f92890f767f106ffa1ddf3c0a54cb7a"}, - {file = "pylint-3.3.2.tar.gz", hash = "sha256:9ec054ec992cd05ad30a6df1676229739a73f8feeabf3912c995d17601052b01"}, -] - -[package.dependencies] -astroid = ">=3.3.5,<=3.4.0.dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} -isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomlkit = ">=0.10.1" - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -description = "OAuthlib authentication support for Requests." -optional = false -python-versions = ">=3.4" -groups = ["main"] -files = [ - {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, - {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, -] - -[package.dependencies] -oauthlib = ">=3.0.0" -requests = ">=2.0.0" - -[package.extras] -rsa = ["oauthlib[signedtoken] (>=3.0.0)"] - -[[package]] -name = "rich" -version = "14.3.3" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["main"] -files = [ - {file = "rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d"}, - {file = "rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "tomlkit" -version = "0.13.2" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "urllib3" -version = "2.2.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "wrapt" -version = "1.17.3" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, - {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, - {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, - {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, - {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, - {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, - {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, - {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, - {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, - {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, - {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, - {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, - {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, - {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, - {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, - {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, - {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, - {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, - {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, - {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, - {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, - {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, - {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, - {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, - {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, - {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, - {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, - {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, - {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, - {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, - {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, - {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, - {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, - {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, - {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, - {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, - {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, - {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, - {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, - {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, - {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, - {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, - {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, - {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, - {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, - {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, - {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, - {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, - {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, - {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, - {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, - {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, - {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, - {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, - {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, - {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, - {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, - {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, - {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, - {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, - {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, - {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, - {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, - {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, - {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, - {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, - {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, -] - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[metadata] -lock-version = "2.1" -python-versions = "^3.12" -content-hash = "3f8fc299a5000c9bf4a0891b28451b46a943baef79bffbc213fd0f5e917a276d" diff --git a/pyproject.toml b/pyproject.toml index 97a6b64..131c416 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,38 +1,27 @@ -[tool.poetry] +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] name = "withings-sync" -version = "1.0.0.dev1" -description = "A tool for synchronisation of Withings (ex. Nokia Health Body) to Garmin Connect and Trainer Road." -license = "MIT" -authors = ["Masayuki Hamasaki ", - "Steffen Vogel ", - "Jarek Hartman ", - ] -maintainers = ["longstone <2110765+longstone@users.noreply.github.com>", - ] -repository = "https://github.com/jaroslawhartman/withings-sync" -homepage = "https://github.com/jaroslawhartman/withings-sync" +version = "2.0.0" +description = "Reliable Withings weight and body-composition sync to Garmin Connect." readme = "README.md" -keywords = ["garmin", "withings", "sync", "api", "scale", "smarthome"] -packages = [{include = "withings_sync"}] -classifiers = ["Topic :: Utilities", - "Topic :: Home Automation", - ] - -[tool.poetry.dependencies] -python = "^3.12" -garth = ">=0.7.11" -requests = ">=2.32.3" -lxml = ">=5.3.0" -python-dotenv = ">=1.0.1" -importlib-resources = ">=6.4.5" +requires-python = ">=3.12" +license = { text = "MIT" } +dependencies = [ + "garminconnect==0.3.17", + "requests>=2.32.3,<3", +] -[tool.poetry.group.dev.dependencies] -black = ">=24.10.0" -pylint = ">=3.3.1" +[project.optional-dependencies] +test = ["pytest>=8,<9"] -[tool.poetry.scripts] +[project.scripts] withings-sync = "withings_sync.sync:main" -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.setuptools.packages.find] +include = ["withings_sync*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/tests/test_fit.py b/tests/test_fit.py new file mode 100644 index 0000000..6f4d294 --- /dev/null +++ b/tests/test_fit.py @@ -0,0 +1,55 @@ +import struct +from datetime import datetime, timezone + +from withings_sync.fit import FitEncoderWeight, _crc + + +def test_weight_scale_file_has_valid_header_crc_and_composition_fields(): + encoder = FitEncoderWeight() + measured_at = datetime(2026, 8, 12, 10, 30, tzinfo=timezone.utc) + encoder.write_file_info(time_created=measured_at) + encoder.write_file_creator() + encoder.write_device_info(measured_at) + encoder.write_weight_scale( + measured_at, + weight=72.35, + percent_fat=21.2, + percent_hydration=54.1, + bone_mass=3.1, + muscle_mass=33.4, + bmi=23.8, + ) + encoder.finish() + data = encoder.getvalue() + + assert data[8:12] == b".FIT" + assert struct.unpack_from("H", data, offset + 2 + )[0] + count = data[offset + 4] + offset += 5 + fields = [] + for _ in range(count): + number, size, base_type = struct.unpack_from("= {0, 1, 2, 4, 5, 13, 253} diff --git a/tests/test_garmin.py b/tests/test_garmin.py new file mode 100644 index 0000000..9256279 --- /dev/null +++ b/tests/test_garmin.py @@ -0,0 +1,32 @@ +import json + +from withings_sync.garmin import GarminClient +from withings_sync.storage import Storage + + +def test_garmin_mfa_prompt_saves_tokens_not_password(tmp_path, monkeypatch): + storage = Storage(tmp_path) + calls = {} + + class FakeTokenClient: + def dump(self, path): + with open(path, "w", encoding="utf-8") as token_file: + json.dump({"di_token": "access", "di_refresh_token": "refresh"}, token_file) + + class FakeGarmin: + def __init__(self, **kwargs): + calls["kwargs"] = kwargs + self.client = FakeTokenClient() + + def login(self, tokenstore): + calls["mfa"] = calls["kwargs"]["prompt_mfa"]() + calls["tokenstore"] = tokenstore + + monkeypatch.setattr("withings_sync.garmin.Garmin", FakeGarmin) + client = GarminClient(storage) + client.authenticate("user@example.com", "do-not-save-this", "123456") + + assert calls["mfa"] == "123456" + assert calls["tokenstore"] == str(tmp_path / "garmin_tokens.json") + assert "do-not-save-this" not in (tmp_path / "garmin_tokens.json").read_text() + assert client.status()[0] diff --git a/tests/test_storage.py b/tests/test_storage.py new file mode 100644 index 0000000..1feb11a --- /dev/null +++ b/tests/test_storage.py @@ -0,0 +1,85 @@ +import json +import os +from datetime import datetime, timezone + +import pytest + +from withings_sync.storage import Storage +from withings_sync.withings import TOKEN_FILE, WithingsClient, WithingsError + + +class Response: + def __init__(self, payload): + self.payload = payload + + def raise_for_status(self): + return None + + def json(self): + return self.payload + + +class Session: + def __init__(self, responses): + self.responses = iter(responses) + self.calls = [] + + def post(self, *args, **kwargs): + self.calls.append((args, kwargs)) + return next(self.responses) + + +def test_json_state_is_atomic_private_and_readable(tmp_path): + storage = Storage(tmp_path) + storage.write_json("credentials.json", {"refresh_token": "secret"}) + + assert storage.read_json("credentials.json") == {"refresh_token": "secret"} + assert os.stat(tmp_path / "credentials.json").st_mode & 0o777 == 0o600 + assert not list(tmp_path.glob("*.tmp")) + + +def test_withings_refresh_rotates_and_persists_token_immediately(tmp_path): + storage = Storage(tmp_path) + storage.write_json( + TOKEN_FILE, + { + "access_token": "old-access", + "refresh_token": "old-refresh", + "expires_at": int(datetime.now(timezone.utc).timestamp()) - 1, + }, + ) + session = Session( + [ + Response( + { + "status": 0, + "body": { + "access_token": "new-access", + "refresh_token": "new-refresh", + "userid": 7, + "expires_in": 3600, + }, + } + ) + ] + ) + client = WithingsClient(storage, "app-id", "app-secret", "https://callback", session) + + assert client.access_token() == "new-access" + saved = storage.read_json(TOKEN_FILE) + assert saved["refresh_token"] == "new-refresh" + assert saved["expires_at"] > int(datetime.now(timezone.utc).timestamp()) + assert session.calls[0][1]["data"]["refresh_token"] == "old-refresh" + + +def test_failed_refresh_does_not_replace_rotating_refresh_token(tmp_path): + storage = Storage(tmp_path) + original = {"access_token": "old", "refresh_token": "still-needed", "expires_at": 0} + storage.write_json(TOKEN_FILE, original) + session = Session([Response({"status": 401, "error": "invalid refresh token"})]) + client = WithingsClient(storage, "id", "secret", "redirect", session) + + with pytest.raises(WithingsError, match="refresh"): + client.access_token() + assert storage.read_json(TOKEN_FILE)["refresh_token"] == "still-needed" + assert json.loads((tmp_path / TOKEN_FILE).read_text()) == original diff --git a/tests/test_sync.py b/tests/test_sync.py new file mode 100644 index 0000000..838e4d0 --- /dev/null +++ b/tests/test_sync.py @@ -0,0 +1,95 @@ +import base64 +from datetime import datetime, timezone +import json + +import pytest + +from withings_sync.garmin import GarminClient, token_expiry +from withings_sync.storage import Storage +from withings_sync.sync import SyncService + + +class FakeWithings: + def __init__(self, groups): + self.groups = groups + self.calls = [] + + def get_measurements(self, start, end): + self.calls.append((start, end)) + return self.groups + + def get_height(self): + return 1.75 + + +class FakeGarmin: + def __init__(self): + self.uploads = [] + + def upload(self, measurement): + self.uploads.append(measurement.source_id) + + +def test_successful_upload_state_deduplicates_repeated_sync(tmp_path): + storage = Storage(tmp_path) + withings = FakeWithings( + [ + { + "grpid": 987, + "date": 1786530600, + "measures": [{"type": 1, "value": 71000, "unit": -3}], + } + ] + ) + garmin = FakeGarmin() + service = SyncService(storage, withings, garmin) + + assert service.run_once() == 1 + assert service.run_once() == 0 + state = storage.read_json("state.json") + assert garmin.uploads == ["987"] + assert state["uploaded_measurements"]["987"] == datetime.fromtimestamp( + 1786530600, timezone.utc + ).isoformat() + + +def test_garmin_state_commits_only_after_upload_succeeds(tmp_path): + storage = Storage(tmp_path) + withings = FakeWithings( + [ + { + "grpid": 988, + "date": 1786530600, + "measures": [{"type": 1, "value": 71000, "unit": -3}], + } + ] + ) + + class FailedGarmin: + def upload(self, measurement): + raise RuntimeError("upload rejected") + + service = SyncService(storage, withings, FailedGarmin()) + with pytest.raises(RuntimeError, match="upload rejected"): + service.run_once() + + assert storage.read_json("state.json") == {} + + +def test_garmin_status_reads_saved_tokens_and_expiry(tmp_path): + storage = Storage(tmp_path) + expiry = int(datetime.now(timezone.utc).timestamp()) + 600 + payload = base64.urlsafe_b64encode( + json.dumps({"exp": expiry}).encode() + ).decode().rstrip("=") + token = f"header.{payload}.signature" + storage.write_json( + "garmin_tokens.json", + {"di_token": token, "di_refresh_token": "refresh-token"}, + ) + + connected, token_expires = GarminClient(storage).status() + + assert connected + assert token_expires == datetime.fromtimestamp(expiry, timezone.utc) + assert token_expiry(None) is None diff --git a/tests/test_withings.py b/tests/test_withings.py new file mode 100644 index 0000000..8f59d51 --- /dev/null +++ b/tests/test_withings.py @@ -0,0 +1,48 @@ +from datetime import datetime, timezone + +from withings_sync.withings import ( + WithingsClient, + parse_measurement_groups, +) + + +def test_parse_weight_composition_and_bmi(): + groups = [ + { + "grpid": 1234, + "date": 1786530600, + "measures": [ + {"type": 1, "value": 72350, "unit": -3}, + {"type": 6, "value": 2120, "unit": -2}, + {"type": 77, "value": 39000, "unit": -3}, + {"type": 76, "value": 32800, "unit": -3}, + {"type": 88, "value": 3100, "unit": -3}, + {"type": 170, "value": 8, "unit": 0}, + ], + }, + {"grpid": 1235, "date": 1786530700, "measures": [{"type": 6, "value": 20}]}, + ] + + measurements = parse_measurement_groups(groups, height_m=1.75) + item = measurements[0] + + assert item.source_id == "1234" + assert item.timestamp == datetime.fromtimestamp(1786530600, timezone.utc) + assert item.weight == 72.35 + assert item.percent_fat == 21.2 + assert item.percent_hydration == 53.9 + assert item.muscle_mass == 32.8 + assert item.bone_mass == 3.1 + assert item.visceral_fat == 8 + assert item.bmi == 23.6 + assert len(measurements) == 1 + + +def test_extract_code_accepts_code_or_validated_redirect_url(): + assert WithingsClient.extract_code(" auth-code ") == "auth-code" + assert ( + WithingsClient.extract_code( + "https://example.org/callback?code=abc&state=s1", "s1" + ) + == "abc" + ) diff --git a/withings_sync/config/withings_app.json b/withings_sync/config/withings_app.json deleted file mode 100644 index 95295d7..0000000 --- a/withings_sync/config/withings_app.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "callback_url": "https://jaroslawhartman.github.io/withings-sync/contrib/withings.html", - "client_id": "183e03e1f363110b3551f96765c98c10e8f1aa647a37067a1cb64bbbaf491626", - "consumer_secret": "a75d655c985d9e6391df1514c16719ef7bd69fa7c5d3fd0eac2e2b0ed48f1765" -} diff --git a/withings_sync/fit.py b/withings_sync/fit.py index 4c2e5f4..fa08cd2 100644 --- a/withings_sync/fit.py +++ b/withings_sync/fit.py @@ -1,316 +1,177 @@ -from io import BytesIO -from struct import pack -from struct import unpack -from datetime import datetime -import time +"""Small FIT encoder for Garmin weight-scale/body-composition records.""" +from __future__ import annotations -def _calcCRC(crc, byte): - table = [0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, - 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400] - # compute checksum of lower four bits of byte - tmp = table[crc & 0xF] - crc = (crc >> 4) & 0x0FFF - crc = crc ^ tmp ^ table[byte & 0xF] - # now compute checksum of upper four bits of byte - tmp = table[crc & 0xF] - crc = (crc >> 4) & 0x0FFF - crc = crc ^ tmp ^ table[(byte >> 4) & 0xF] - return crc - - -class FitBaseType(object): - """BaseType Definition - - see FIT Protocol Document(Page.20)""" - enum = {'#': 0, 'endian': 0, 'field': 0x00, 'name': 'enum', 'invalid': 0xFF, 'size': 1} - sint8 = {'#': 1, 'endian': 0, 'field': 0x01, 'name': 'sint8', 'invalid': 0x7F, 'size': 1} - uint8 = {'#': 2, 'endian': 0, 'field': 0x02, 'name': 'uint8', 'invalid': 0xFF, 'size': 1} - sint16 = {'#': 3, 'endian': 1, 'field': 0x83, 'name': 'sint16', 'invalid': 0x7FFF, 'size': 2} - uint16 = {'#': 4, 'endian': 1, 'field': 0x84, 'name': 'uint16', 'invalid': 0xFFFF, 'size': 2} - sint32 = {'#': 5, 'endian': 1, 'field': 0x85, 'name': 'sint32', 'invalid': 0x7FFFFFFF, 'size': 4} - uint32 = {'#': 6, 'endian': 1, 'field': 0x86, 'name': 'uint32', 'invalid': 0xFFFFFFFF, 'size': 4} - string = {'#': 7, 'endian': 0, 'field': 0x07, 'name': 'string', 'invalid': 0x00, 'size': 1} - float32 = {'#': 8, 'endian': 1, 'field': 0x88, 'name': 'float32', 'invalid': 0xFFFFFFFF, 'size': 2} - float64 = {'#': 9, 'endian': 1, 'field': 0x89, 'name': 'float64', 'invalid': 0xFFFFFFFFFFFFFFFF, 'size': 4} - uint8z = {'#': 10, 'endian': 0, 'field': 0x0A, 'name': 'uint8z', 'invalid': 0x00, 'size': 1} - uint16z = {'#': 11, 'endian': 1, 'field': 0x8B, 'name': 'uint16z', 'invalid': 0x0000, 'size': 2} - uint32z = {'#': 12, 'endian': 1, 'field': 0x8C, 'name': 'uint32z', 'invalid': 0x00000000, 'size': 4} - byte = {'#': 13, 'endian': 0, 'field': 0x0D, 'name': 'byte', 'invalid': 0xFF, - 'size': 1} # array of byte, field is invalid if all bytes are invalid - - @staticmethod - def get_format(basetype): - formats = { - 0: 'B', 1: 'b', 2: 'B', 3: 'h', 4: 'H', 5: 'i', 6: 'I', 7: 's', 8: 'f', - 9: 'd', 10: 'B', 11: 'H', 12: 'I', 13: 'c', - } - return formats[basetype['#']] - - @staticmethod - def pack(basetype, value): - """function to avoid DeprecationWarning""" - if basetype['#'] in (1, 2, 3, 4, 5, 6, 10, 11, 12): - value = int(value) - fmt = FitBaseType.get_format(basetype) - return pack(fmt, value) +from datetime import datetime, timezone +import struct +from typing import Any +FIT_EPOCH = 631065600 +CRC_TABLE = ( + 0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, + 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400, +) +UINT8 = (2, 1, 0x02, 0xFF, "B") +UINT16 = (4, 2, 0x84, 0xFFFF, "H") +UINT32 = (6, 4, 0x86, 0xFFFFFFFF, "I") +UINT32Z = (12, 4, 0x8C, 0, "I") +ENUM = (0, 1, 0x00, 0xFF, "B") -class Fit(object): - HEADER_SIZE = 12 - # not sure if this is the mesg_num - GMSG_NUMS = { - 'file_id': 0, - 'device_info': 23, - 'weight_scale': 30, - 'file_creator': 49, - 'blood_pressure': 51, - } +def _crc_byte(crc: int, byte: int) -> int: + tmp = CRC_TABLE[crc & 0x0F] + crc = (crc >> 4) & 0x0FFF + crc ^= tmp ^ CRC_TABLE[byte & 0x0F] + tmp = CRC_TABLE[crc & 0x0F] + crc = (crc >> 4) & 0x0FFF + crc ^= tmp ^ CRC_TABLE[(byte >> 4) & 0x0F] + return crc -class FitEncoder(Fit): - FILE_TYPE = 9 - LMSG_TYPE_FILE_INFO = 0 - LMSG_TYPE_FILE_CREATOR = 1 - LMSG_TYPE_DEVICE_INFO = 2 +def _crc(data: bytes) -> int: + result = 0 + for byte in data: + result = _crc_byte(result, byte) + return result - def __init__(self): - self.buf = BytesIO() - self.write_header() # create header first - self.device_info_defined = False - def __str__(self): - orig_pos = self.buf.tell() - self.buf.seek(0) - lines = [] - while True: - b = self.buf.read(16) - if not b: - break - lines.append(' '.join(['%02x' % ord(c) for c in b])) - self.buf.seek(orig_pos) - return '\n'.join(lines) +def _fit_timestamp(value: datetime | int | float) -> int: + if isinstance(value, datetime): + if value.tzinfo is None: + value = value.replace(tzinfo=timezone.utc) + value = value.timestamp() + result = int(value) - FIT_EPOCH + if result < 0 or result >= UINT32[3]: + raise ValueError("Timestamp is outside the FIT uint32 range.") + return result - def write_header(self, header_size=Fit.HEADER_SIZE, - protocol_version=16, - profile_version=108, - data_size=0, - data_type=b'.FIT'): - self.buf.seek(0) - s = pack('BBHI4s', header_size, protocol_version, profile_version, data_size, data_type) - self.buf.write(s) - def _build_content_block(self, content): - field_defs = [] - values = [] - for num, basetype, value, scale in content: - s = pack('BBB', num, basetype['size'], basetype['field']) - field_defs.append(s) - if value is None: - # invalid value - value = basetype['invalid'] - elif scale is not None: - value *= scale - values.append(FitBaseType.pack(basetype, value)) - return (b''.join(field_defs), b''.join(values)) +class FitEncoderWeight: + """Encode a single Garmin FIT weight-scale file.""" - def write_file_info(self, serial_number=None, time_created=None, manufacturer=None, product=None, number=None): - if time_created is None: - time_created = datetime.now() + def __init__(self) -> None: + self.data = bytearray() + self._file_defined = False + self._creator_defined = False + self._device_defined = False + self._weight_defined = False - content = [ - (3, FitBaseType.uint32z, serial_number, None), - (4, FitBaseType.uint32, self.timestamp(time_created), None), - (1, FitBaseType.uint16, manufacturer, None), - (2, FitBaseType.uint16, product, None), - (5, FitBaseType.uint16, number, None), - (0, FitBaseType.enum, self.FILE_TYPE, None), # type + @staticmethod + def _pack_value(base_type: tuple[int, int, int, int, str], value: Any) -> bytes: + _, _, _, invalid, fmt = base_type + if value is None: + value = invalid + else: + value = int(round(value)) + if value < 0 or value >= invalid: + raise ValueError(f"FIT value is outside {fmt} range.") + return struct.pack("<" + fmt, value) + + def _write_message( + self, + local: int, + fields: list[tuple[int, tuple[int, int, int, int, str], Any]], + values: list[Any] | None = None, + definition: bool = True, + ) -> None: + if definition: + global_message = {0: 0, 1: 49, 2: 23, 3: 30}[local] + self.data.extend( + struct.pack(" None: + created = time_created or datetime.now(timezone.utc) + values = [ + serial_number, + _fit_timestamp(created), + manufacturer, + product, + number, + 9, ] - fields, values = self._build_content_block(content) - - # create fixed content - msg_number = self.GMSG_NUMS['file_id'] - fixed_content = pack('BBHB', 0, 0, msg_number, len(content)) # reserved, architecture(0: little endian) - - self.buf.write(b''.join([ - # definition - self.record_header(definition=True, lmsg_type=self.LMSG_TYPE_FILE_INFO), - fixed_content, - fields, - # record - self.record_header(lmsg_type=self.LMSG_TYPE_FILE_INFO), - values, - ])) - - def write_file_creator(self, software_version=None, hardware_version=None): - content = [ - (0, FitBaseType.uint16, software_version, None), - (1, FitBaseType.uint8, hardware_version, None), + fields = [ + (3, UINT32Z, values[0]), (4, UINT32, values[1]), (1, UINT16, values[2]), + (2, UINT16, values[3]), (5, UINT16, values[4]), (0, ENUM, values[5]), ] - fields, values = self._build_content_block(content) - - msg_number = self.GMSG_NUMS['file_creator'] - fixed_content = pack('BBHB', 0, 0, msg_number, len(content)) # reserved, architecture(0: little endian) - self.buf.write(b''.join([ - # definition - self.record_header(definition=True, lmsg_type=self.LMSG_TYPE_FILE_CREATOR), - fixed_content, - fields, - # record - self.record_header(lmsg_type=self.LMSG_TYPE_FILE_CREATOR), - values, - ])) - - def write_device_info(self, timestamp, serial_number=None, cum_operationg_time=None, manufacturer=None, - product=None, software_version=None, battery_voltage=None, device_index=None, - device_type=None, hardware_version=None, battery_status=None): - content = [ - (253, FitBaseType.uint32, self.timestamp(timestamp), 1), - (3, FitBaseType.uint32z, serial_number, 1), - (7, FitBaseType.uint32, cum_operationg_time, 1), - (8, FitBaseType.uint32, None, None), # unknown field(undocumented) - (2, FitBaseType.uint16, manufacturer, 1), - (4, FitBaseType.uint16, product, 1), - (5, FitBaseType.uint16, software_version, 100), - (10, FitBaseType.uint16, battery_voltage, 256), - (0, FitBaseType.uint8, device_index, 1), - (1, FitBaseType.uint8, device_type, 1), - (6, FitBaseType.uint8, hardware_version, 1), - (11, FitBaseType.uint8, battery_status, None), + self._write_message(0, fields, values, definition=not self._file_defined) + self._file_defined = True + + def write_file_creator( + self, software_version: int | None = None, hardware_version: int | None = None + ) -> None: + fields = [(0, UINT16, software_version), (1, UINT8, hardware_version)] + self._write_message(1, fields, definition=not self._creator_defined) + self._creator_defined = True + + def write_device_info(self, timestamp: datetime | int | float) -> None: + fields: list[tuple[int, tuple[int, int, int, int, str], Any]] = [ + (253, UINT32, None), (3, UINT32Z, None), (7, UINT32, None), + (8, UINT32, None), (2, UINT16, None), (4, UINT16, None), + (5, UINT16, None), (10, UINT16, None), (0, UINT8, None), + (1, UINT8, None), (6, UINT8, None), (11, UINT8, None), ] - fields, values = self._build_content_block(content) - - if not self.device_info_defined: - header = self.record_header(definition=True, lmsg_type=self.LMSG_TYPE_DEVICE_INFO) - msg_number = self.GMSG_NUMS['device_info'] - fixed_content = pack('BBHB', 0, 0, msg_number, len(content)) # reserved, architecture(0: little endian) - self.buf.write(header + fixed_content + fields) - self.device_info_defined = True - - header = self.record_header(lmsg_type=self.LMSG_TYPE_DEVICE_INFO) - self.buf.write(header + values) - - def record_header(self, definition=False, lmsg_type=0): - msg = 0 - if definition: - msg = 1 << 6 # 6th bit is a definition message - return pack('B', msg + lmsg_type) - - def crc(self): - orig_pos = self.buf.tell() - self.buf.seek(0) - - crc = 0 - while True: - b = self.buf.read(1) - if not b: - break - crc = _calcCRC(crc, unpack('b', b)[0]) - self.buf.seek(orig_pos) - return pack('H', crc) - - def finish(self): - """re-weite file-header, then append crc to end of file""" - data_size = self.get_size() - self.HEADER_SIZE - self.write_header(data_size=data_size) - crc = self.crc() - self.buf.seek(0, 2) - self.buf.write(crc) - - def get_size(self): - orig_pos = self.buf.tell() - self.buf.seek(0, 2) - size = self.buf.tell() - self.buf.seek(orig_pos) - return size - - def getvalue(self): - return self.buf.getvalue() - - def timestamp(self, t): - """the timestamp in fit protocol is seconds since - UTC 00:00 Dec 31 1989 (631065600)""" - if isinstance(t, datetime): - t = time.mktime(t.timetuple()) - return t - 631065600 - - -class FitEncoderBloodPressure(FitEncoder): - # Here might be dragons - no idea what lsmg stand for, found 14 somewhere in the deepest web - LMSG_TYPE_BLOOD_PRESSURE = 14 - - def __init__(self): - super().__init__() - self.blood_pressure_monitor_defined = False - - def write_blood_pressure(self, - timestamp, - diastolic_blood_pressure=None, - systolic_blood_pressure=None, - mean_arterial_pressure=None, - map_3_sample_mean=None, - map_morning_values=None, - map_evening_values=None, - heart_rate=None, ): - # BLOOD PRESSURE FILE MESSAGES - content = [ - (253, FitBaseType.uint32, self.timestamp(timestamp), 1), - (0, FitBaseType.uint16, systolic_blood_pressure, 1), - (1, FitBaseType.uint16, diastolic_blood_pressure, 1), - (2, FitBaseType.uint16, mean_arterial_pressure, 1), - (3, FitBaseType.uint16, map_3_sample_mean, 1), - (4, FitBaseType.uint16, map_morning_values, 1), - (5, FitBaseType.uint16, map_evening_values, 1), - (6, FitBaseType.uint8, heart_rate, 1), + values = [_fit_timestamp(timestamp)] + [None] * (len(fields) - 1) + self._write_message(2, fields, values, definition=not self._device_defined) + self._device_defined = True + + def write_weight_scale( + self, + timestamp: datetime | int | float, + weight: int | float, + percent_fat: int | float | None = None, + percent_hydration: int | float | None = None, + visceral_fat_mass: int | float | None = None, + bone_mass: int | float | None = None, + muscle_mass: int | float | None = None, + basal_met: int | float | None = None, + active_met: int | float | None = None, + physique_rating: int | float | None = None, + metabolic_age: int | float | None = None, + visceral_fat_rating: int | float | None = None, + bmi: int | float | None = None, + ) -> None: + fields = [ + (253, UINT32, _fit_timestamp(timestamp)), + (0, UINT16, _scaled(weight, 100)), + (1, UINT16, _scaled(percent_fat, 100)), + (2, UINT16, _scaled(percent_hydration, 100)), + (3, UINT16, _scaled(visceral_fat_mass, 100)), + (4, UINT16, _scaled(bone_mass, 100)), + (5, UINT16, _scaled(muscle_mass, 100)), + (7, UINT16, _scaled(basal_met, 4)), + (9, UINT16, _scaled(active_met, 4)), + (8, UINT8, physique_rating), + (10, UINT8, metabolic_age), + (11, UINT8, visceral_fat_rating), + (13, UINT16, _scaled(bmi, 10)), ] - fields, values = self._build_content_block(content) - - if not self.blood_pressure_monitor_defined: - header = self.record_header(definition=True, lmsg_type=self.LMSG_TYPE_BLOOD_PRESSURE) - msg_number = self.GMSG_NUMS['blood_pressure'] - fixed_content = pack('BBHB', 0, 0, msg_number, len(content)) # reserved, architecture(0: little endian) - self.buf.write(header + fixed_content + fields) - self.blood_pressure_monitor_defined = True + self._write_message(3, fields, definition=not self._weight_defined) + self._weight_defined = True - header = self.record_header(lmsg_type=self.LMSG_TYPE_BLOOD_PRESSURE) - self.buf.write(header + values) + def finish(self) -> None: + data_size = len(self.data) + header = struct.pack(" bytes: + return bytes(self.data) -class FitEncoderWeight(FitEncoder): - LMSG_TYPE_WEIGHT_SCALE = 3 - - def __init__(self): - super().__init__() - self.weight_scale_defined = False - - def write_weight_scale(self, timestamp, weight, percent_fat=None, percent_hydration=None, - visceral_fat_mass=None, bone_mass=None, muscle_mass=None, basal_met=None, - active_met=None, physique_rating=None, metabolic_age=None, - visceral_fat_rating=None, bmi=None): - content = [ - (253, FitBaseType.uint32, self.timestamp(timestamp), 1), - (0, FitBaseType.uint16, weight, 100), - (1, FitBaseType.uint16, percent_fat, 100), - (2, FitBaseType.uint16, percent_hydration, 100), - (3, FitBaseType.uint16, visceral_fat_mass, 100), - (4, FitBaseType.uint16, bone_mass, 100), - (5, FitBaseType.uint16, muscle_mass, 100), - (7, FitBaseType.uint16, basal_met, 4), - (9, FitBaseType.uint16, active_met, 4), - (8, FitBaseType.uint8, physique_rating, 1), - (10, FitBaseType.uint8, metabolic_age, 1), - (11, FitBaseType.uint8, visceral_fat_rating, 1), - (13, FitBaseType.uint16, bmi, 10), - ] - fields, values = self._build_content_block(content) - - if not self.weight_scale_defined: - header = self.record_header(definition=True, lmsg_type=self.LMSG_TYPE_WEIGHT_SCALE) - msg_number = self.GMSG_NUMS['weight_scale'] - fixed_content = pack('BBHB', 0, 0, msg_number, len(content)) # reserved, architecture(0: little endian) - self.buf.write(header + fixed_content + fields) - self.weight_scale_defined = True - header = self.record_header(lmsg_type=self.LMSG_TYPE_WEIGHT_SCALE) - self.buf.write(header + values) +def _scaled(value: int | float | None, factor: int) -> int | float | None: + return None if value is None else value * factor diff --git a/withings_sync/garmin.py b/withings_sync/garmin.py index 58165a2..0089cbf 100644 --- a/withings_sync/garmin.py +++ b/withings_sync/garmin.py @@ -1,118 +1,143 @@ -"""This module handles the Garmin connectivity.""" +"""Garmin Connect authentication and FIT body-composition uploads.""" -import io +from __future__ import annotations + +import base64 +from datetime import datetime, timezone +import getpass +import json import logging -import os - -import garth -# Temporary fix until Garth project merges https://github.com/matin/garth/issues/73 -garth.http.USER_AGENT = {"User-Agent": ("GCM-iOS-5.7.2.1")} - -log = logging.getLogger("garmin") - -HOME = os.getenv("HOME", ".") -GARMIN_SESSION = os.path.abspath(os.path.expanduser(os.getenv('GARMIN_SESSION', os.path.join(HOME, ".garmin_session")))) - - -class LoginFailed(Exception): - """Raised when login fails.""" - - -class APIException(Exception): - """Raised for API exceptions.""" - - -class GarminConnect: - """Main GarminConnect class.""" - - def __init__(self, config_folder=None) -> None: - self.client = garth.Client() - self.config_folder = config_folder - - if config_folder: - self.session_path = os.path.join(config_folder, ".garmin_session") - else: - self.session_path = GARMIN_SESSION - - # Log helpful message if using new config folder and file doesn't exist - if config_folder and not os.path.exists(self.session_path): - home = os.getenv("HOME", ".") - legacy_path = os.path.abspath(os.path.expanduser(os.path.join(home, ".garmin_session"))) - if os.path.exists(legacy_path): - log.info(f"Using new config folder: {self.session_path}") - log.info(f"If you want to use existing session, copy from: {legacy_path}") - - def login(self, email=None, password=None): - """Login to Garmin Connect with session persistence and MFA support.""" - log.debug("Attempting Garmin login") - - if GarminConnect.invalid_garmin_session_config(self): - raise APIException("invalid garmin session path config") - - if os.path.exists(self.session_path): - try: - log.debug("Loading existing Garmin session") - self.client.load(self.session_path) - if self.looks_like_valid_session(): - log.info(f"Successfully loaded Garmin session for user: {self.client.username}") - return - else: - log.warning("Session file exists but appears invalid or expired") - except Exception as ex: - log.warning(f"Failed to load Garmin session: {ex}") - - # Fallback to credential authentication - if not email or not password: - raise APIException( - "No valid session found and no credentials provided. " - "For MFA accounts:" - "1) Authenticate once using Garmin Connect mobile app or web interface, " - "2) Locate the .garmin_session file in your home directory, " - "3) Copy this file to the location specified by GARMIN_SESSION environment variable." - ) - - # Check write permissions BEFORE attempting authentication - session_dir = os.path.dirname(self.session_path) - if session_dir and not os.access(session_dir, os.W_OK): - log.warning(f"Cannot write to session directory: {session_dir}") - +from typing import Callable + +from garminconnect import Garmin +from garminconnect.exceptions import ( + GarminConnectAuthenticationError, + GarminConnectConnectionError, +) + +from withings_sync.fit import FitEncoderWeight +from withings_sync.storage import Storage +from withings_sync.withings import Measurement + +log = logging.getLogger(__name__) +GARMIN_TOKEN_FILE = "garmin_tokens.json" +REAUTH_MESSAGE = ( + "Garmin authentication failed. Run: " + "docker compose run --rm withings-sync auth garmin" +) + + +def token_expiry(token: str | None) -> datetime | None: + if not token: + return None + try: + payload = token.split(".")[1] + payload += "=" * (-len(payload) % 4) + claims = json.loads(base64.urlsafe_b64decode(payload)) + return datetime.fromtimestamp(int(claims["exp"]), timezone.utc) + except (IndexError, KeyError, TypeError, ValueError, json.JSONDecodeError): + return None + + +class GarminClient: + def __init__(self, storage: Storage): + self.storage = storage + self.token_path = storage.directory / GARMIN_TOKEN_FILE + self.client: Garmin | None = None + + def status(self) -> tuple[bool, datetime | None]: try: - log.info("Attempting Garmin authentication with credentials") - self.client.login(email, password) - log.info("Garmin authentication successful") - - except Exception as ex: - raise APIException( - f"Authentication failure: {ex}. " - f"For MFA accounts, credential-based login may not work. " - f"Use the session file method described in the error message above." - ) - - # Save session separately to handle dump failures distinctly + data = json.loads(self.token_path.read_text(encoding="utf-8")) + except FileNotFoundError: + return False, None + except (OSError, json.JSONDecodeError) as exc: + raise RuntimeError(f"Cannot read Garmin token file: {exc}") from exc + expiry = token_expiry(data.get("di_token")) + return bool(data.get("di_refresh_token") or data.get("di_token")), expiry + + def _login( + self, + email: str | None = None, + password: str | None = None, + prompt_mfa: Callable[[], str] | None = None, + ) -> Garmin: + self.token_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + client = Garmin(email=email, password=password, prompt_mfa=prompt_mfa) + client.login(tokenstore=str(self.token_path)) + self.client = client + self._persist() + return client + + def connect(self) -> Garmin: + if self.client: + return self.client try: - # Ensure parent directory exists before dumping session - if session_dir: - os.makedirs(session_dir, exist_ok=True) - log.debug("Session directory created/verified") - - self.client.dump(self.session_path) - log.info(f"Successfully saved Garmin session to {self.session_path}") - - except Exception as ex: - raise APIException( - f"Session save failed: {ex}. Authentication succeeded but session could not be persisted. " - f"Check GARMIN_SESSION path, permissions, and available disk space." - ) + return self._login() + except GarminConnectAuthenticationError as exc: + raise RuntimeError(f"{REAUTH_MESSAGE}. Details: {exc}") from exc + except Exception as exc: + raise RuntimeError(f"Garmin Connect is unavailable: {exc}") from exc - def looks_like_valid_session(self) -> bool: - return hasattr(self.client, "username") and self.client.username + def authenticate( + self, + email: str, + password: str, + mfa_code: str | None = None, + ) -> None: + def prompt() -> str: + return mfa_code if mfa_code else input("Garmin MFA code: ").strip() - def invalid_garmin_session_config(self) -> bool: - return not self.session_path + self._login(email=email, password=password, prompt_mfa=prompt) + self._persist() - def upload_file(self, ffile): - """Upload fit file to Garmin Connect.""" - fit_file = io.BytesIO(ffile.getvalue()) - fit_file.name = "withings.fit" - self.client.upload(fit_file) - return True + def authenticate_interactively(self, mfa_code: str | None = None) -> None: + has_tokens, expiry = self.status() + if has_tokens: + try: + self._login() + log.info( + "Existing Garmin token is valid; no password is needed%s.", + f" (access expires {expiry.isoformat()})" if expiry else "", + ) + return + except GarminConnectAuthenticationError: + log.info("Saved Garmin credentials were rejected; a new login is required.") + except GarminConnectConnectionError as exc: + raise RuntimeError(f"Garmin login could not reach Connect: {exc}") from exc + email = input("Garmin email: ").strip() + password = getpass.getpass("Garmin password: ") + self.authenticate(email, password, mfa_code) + + def _persist(self) -> None: + if not self.client: + raise RuntimeError("Garmin client has no authenticated session.") + self.client.client.dump(str(self.token_path)) + + def upload(self, measurement: Measurement) -> None: + client = self.connect() + encoder = FitEncoderWeight() + encoder.write_file_info(time_created=measurement.timestamp) + encoder.write_file_creator() + encoder.write_device_info(timestamp=measurement.timestamp) + encoder.write_weight_scale( + timestamp=measurement.timestamp, + weight=measurement.weight, + percent_fat=measurement.percent_fat, + percent_hydration=measurement.percent_hydration, + bone_mass=measurement.bone_mass, + muscle_mass=measurement.muscle_mass, + visceral_fat_rating=measurement.visceral_fat, + bmi=measurement.bmi, + ) + encoder.finish() + try: + client.client.post( + "connectapi", + "/upload-service/upload", + files={"file": ("body_composition.fit", encoder.getvalue())}, + api=True, + ) + except GarminConnectAuthenticationError as exc: + raise RuntimeError(f"{REAUTH_MESSAGE}. Details: {exc}") from exc + finally: + self._persist() diff --git a/withings_sync/storage.py b/withings_sync/storage.py new file mode 100644 index 0000000..5ef2f43 --- /dev/null +++ b/withings_sync/storage.py @@ -0,0 +1,63 @@ +"""Durable local state shared by authentication and sync commands.""" + +from __future__ import annotations + +from contextlib import contextmanager +import fcntl +import json +import os +from pathlib import Path +import tempfile +from typing import Any, Iterator + + +class Storage: + def __init__(self, directory: str | Path): + self.directory = Path(directory) + self.directory.mkdir(mode=0o700, parents=True, exist_ok=True) + + @contextmanager + def lock(self) -> Iterator[None]: + lock_path = self.directory / "withings-sync.lock" + with lock_path.open("a", encoding="utf-8") as lock_file: + os.chmod(lock_path, 0o600) + fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX) + try: + yield + finally: + fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) + + def read_json(self, name: str) -> dict[str, Any]: + path = self.directory / name + try: + with path.open(encoding="utf-8") as file: + data = json.load(file) + except FileNotFoundError: + return {} + if not isinstance(data, dict): + raise ValueError(f"Invalid state file: {path}") + return data + + def write_json(self, name: str, data: dict[str, Any]) -> None: + path = self.directory / name + fd, temp_name = tempfile.mkstemp(prefix=f".{name}.", dir=self.directory) + try: + os.fchmod(fd, 0o600) + with os.fdopen(fd, "w", encoding="utf-8") as file: + json.dump(data, file, indent=2, sort_keys=True) + file.write("\n") + file.flush() + os.fsync(file.fileno()) + os.replace(temp_name, path) + os.chmod(path, 0o600) + dir_fd = os.open(self.directory, os.O_RDONLY) + try: + os.fsync(dir_fd) + finally: + os.close(dir_fd) + except BaseException: + try: + os.unlink(temp_name) + except FileNotFoundError: + pass + raise diff --git a/withings_sync/sync.py b/withings_sync/sync.py index 840e90b..58bd97d 100644 --- a/withings_sync/sync.py +++ b/withings_sync/sync.py @@ -1,628 +1,180 @@ -"""This module syncs measurement data from Withings to Garmin a/o TrainerRoad.""" +"""Command-line application and scheduled Withings-to-Garmin synchronization.""" + +from __future__ import annotations import argparse -import time -import sys -import os +from datetime import datetime, time, timedelta, timezone import logging -import json -from datetime import date, datetime -from importlib.metadata import version -import dotenv - -from withings_sync.withings2 import WithingsAccount -from withings_sync.garmin import GarminConnect -from withings_sync.trainerroad import TrainerRoad -from withings_sync.fit import FitEncoderWeight, FitEncoderBloodPressure - -# Load the environment variables from a .env (dotenv) file. -# This is done prior to importing other modules such that all variables, -# also the ones accessed in those modules, can be set in the dotenv file. -dotenv.load_dotenv() - - -def load_variable(env_var, secrets_file): - """Load a variable from an environment variable or from a secrets file""" - # Try to read the value from the secrets file. Silently fail if the file - # cannot be read and use an empty value - try: - with open(secrets_file, encoding="utf-8") as secret: - value = secret.read().strip("\n") - except OSError: - value = "" - - # Load variable from environment if it exists, otherwise use the - # value read from the secrets file. - return os.getenv(env_var, value) - - -GARMIN_USERNAME = load_variable("GARMIN_USERNAME", "/run/secrets/garmin_username") -GARMIN_PASSWORD = load_variable("GARMIN_PASSWORD", "/run/secrets/garmin_password") -TRAINERROAD_USERNAME = load_variable( - "TRAINERROAD_USERNAME", "/run/secrets/trainerroad_username" -) -TRAINERROAD_PASSWORD = load_variable( - "TRAINERROAD_PASSWORD", "/run/secrets/trainerroad_password" +import os +import re +import sys +import time as clock +from typing import Any + +from withings_sync.garmin import GarminClient +from withings_sync.storage import Storage +from withings_sync.withings import ( + WithingsClient, + new_oauth_state, + parse_measurement_groups, ) +STATE_FILE = "state.json" +log = logging.getLogger("withings_sync") -def get_args(): - """get command-line arguments""" - parser = argparse.ArgumentParser( - description=( - "A tool for synchronisation of Withings " - "(ex. Nokia Health Body) to Garmin Connect" - " and Trainer Road or to provide a json string." - ) - ) - - def date_parser(date_string): - return datetime.strptime(date_string, "%Y-%m-%d") - - parser.add_argument( - "--version", "-V", action="version", version=version("withings-sync") - ) - parser.add_argument( - "--garmin-username", - "--gu", - default=GARMIN_USERNAME, - type=str, - metavar="GARMIN_USERNAME", - help="Username to log in to Garmin Connect.", - ) - parser.add_argument( - "--garmin-password", - "--gp", - default=GARMIN_PASSWORD, - type=str, - metavar="GARMIN_PASSWORD", - help="Password to log in to Garmin Connect.", - ) - - parser.add_argument( - "--trainerroad-username", - "--tu", - default=TRAINERROAD_USERNAME, - type=str, - metavar="TRAINERROAD_USERNAME", - help="Username to log in to TrainerRoad.", - ) - - parser.add_argument( - "--trainerroad-password", - "--tp", - default=TRAINERROAD_PASSWORD, - type=str, - metavar="TRAINERROAD_PASSWORD", - help="Password to log in to TrainerRoad.", - ) - - parser.add_argument( - "--fromdate", - "-f", - type=date_parser, - metavar="DATE", - help="Date to start syncing from. Ex: 2023-12-20", - ) - - parser.add_argument( - "--todate", - "-t", - type=date_parser, - default=date.today(), - metavar="DATE", - help="Date for the last sync. Ex: 2023-12-30", - ) - - parser.add_argument( - "--to-fit", - "-F", - action="store_true", - help="Write output file in FIT format.", - ) - - parser.add_argument( - "--to-json", - "-J", - action="store_true", - help="Write output file in JSON format.", - ) - - parser.add_argument( - "--output", - "-o", - type=str, - metavar="BASENAME", - help="Write downloaded measurements to file.", - ) - - parser.add_argument( - "--no-upload", - action="store_true", - help="Won't upload to Garmin Connect or TrainerRoad.", - ) - - parser.add_argument( - "--features", - nargs="+", - default=[], - metavar="BLOOD_PRESSURE", - help="Enable Features like BLOOD_PRESSURE.", - ) - - log_level_group = parser.add_mutually_exclusive_group() - log_level_group.add_argument("--verbose", "-v", action="store_true", help="Run verbosely.") - log_level_group.add_argument("--silent", "-s", action="store_true", help="Run silently (suppress INFO messages).") - - parser.add_argument( - "--dump-raw", - "-R", - action="store_true", - help=( - "Dump the raw Withings API JSON for the selected date range to file. " - "If --output is provided, the file will be BASENAME.withings_raw.json. " - "Otherwise, a default filename with the date range will be used." - ), - ) - - parser.add_argument( - "--config-folder", - "-c", - type=str, - metavar="CONFIG_FOLDER", - help="Path to config folder for session files (if not specified, uses legacy paths in home directory)", - ) - - return parser.parse_args() - - -def sync_garmin(fit_file, config_folder=None): - """Sync generated fit file to Garmin Connect""" - garmin = GarminConnect(config_folder=config_folder) - garmin.login(ARGS.garmin_username, ARGS.garmin_password) - return garmin.upload_file(fit_file) - - -def sync_trainerroad(last_weight): - """Sync measured weight to TrainerRoad""" - t_road = TrainerRoad(ARGS.trainerroad_username, ARGS.trainerroad_password) - t_road.connect() - logging.info("Current TrainerRoad weight: %s kg ", t_road.weight) - logging.info("Updating TrainerRoad weight to %s kg", last_weight) - wt = round(last_weight, 1) - t_road.weight = wt - t_road.disconnect() - - return wt - - -def generate_fitdata(syncdata): - """Generate fit data from measured data""" - logging.debug("Generating fit data...") - - weight_measurements = list(filter(lambda x: (x["type"] == "weight"), syncdata)) - blood_pressure_measurements = list( - filter(lambda x: (x["type"] == "blood_pressure"), syncdata) - ) - - fit_weight = None - fit_blood_pressure = None - - if len(weight_measurements) > 0: - fit_weight = FitEncoderWeight() - fit_weight.write_file_info() - fit_weight.write_file_creator() - - for record in weight_measurements: - fit_weight.write_device_info(timestamp=record.get("date_time")) - fit_weight.write_weight_scale( - timestamp=record.get("date_time"), - weight=record.get("weight"), - percent_fat=record.get("fat_ratio"), - percent_hydration=record.get("percent_hydration"), - bone_mass=record.get("bone_mass"), - muscle_mass=record.get("muscle_mass"), - bmi=record.get("bmi"), - ) - - fit_weight.finish() - else: - logging.info("No weight data to sync for FIT file") - - if len(blood_pressure_measurements) > 0: - fit_blood_pressure = FitEncoderBloodPressure() - fit_blood_pressure.write_file_info() - fit_blood_pressure.write_file_creator() - - for record in blood_pressure_measurements: - fit_blood_pressure.write_device_info(timestamp=record.get("date_time")) - fit_blood_pressure.write_blood_pressure( - timestamp=record.get("date_time"), - diastolic_blood_pressure=record.get("diastolic_blood_pressure"), - systolic_blood_pressure=record.get("systolic_blood_pressure"), - heart_rate=record.get("heart_pulse"), - ) - - fit_blood_pressure.finish() - else: - logging.info("No blood pressure data to sync for FIT file") - - logging.debug("Fit data generated...") - return fit_weight, fit_blood_pressure - - -def generate_jsondata(syncdata): - """Generate fit data from measured data""" - logging.debug("Generating json data...") - - json_data = {} - for record in syncdata: - sdt = str(record["date_time"]) - json_data[sdt] = {} - for dataentry in record["raw_data"]: - for k, jd in dataentry.json_dict().items(): - json_data[sdt][k] = jd - if "bmi" in record: - json_data[sdt]["BMI"] = {"Value": record["bmi"], "Unit": "kg/m^2"} - if "percent_hydration" in record: - json_data[sdt]["Percent_Hydration"] = { - "Value": record["percent_hydration"], - "Unit": "%", - } - logging.debug("Json data generated...") - return json_data - - -def prepare_syncdata(height, groups): - """Prepare measurement data to be sent""" - syncdata = [] - - last_date_time = None - last_weight = None - - sync_dict = {} - - for group in groups: - # Get extra physical measurements - dt = group.get_datetime() - # create a default group_data - group_data = { - "date_time": group.get_datetime(), - "type": "None", - "raw_data": group.get_raw_data(), - } - - if dt not in sync_dict: - sync_dict[dt] = {} - - if group.get_weight(): - group_data = { - "date_time": group.get_datetime(), - "height": height, - "weight": group.get_weight(), - "fat_ratio": group.get_fat_ratio(), - "muscle_mass": group.get_muscle_mass(), - "hydration": group.get_hydration(), - "percent_hydration": None, - "bone_mass": group.get_bone_mass(), - "pulse_wave_velocity": group.get_pulse_wave_velocity(), - "heart_pulse": group.get_heart_pulse(), - "bmi": None, - "raw_data": group.get_raw_data(), - "type": "weight", - } - elif group.get_diastolic_blood_pressure(): - group_data = { - "date_time": group.get_datetime(), - "diastolic_blood_pressure": group.get_diastolic_blood_pressure(), - "systolic_blood_pressure": group.get_systolic_blood_pressure(), - "heart_pulse": group.get_heart_pulse(), - "raw_data": group.get_raw_data(), - "type": "blood_pressure", - } - - # execute the code below, if this is not a whitelisted entry like weight and blood pressure - if group_data["type"] == "None" or ( - group_data["type"] == "blood_pressure" - and "BLOOD_PRESSURE" not in ARGS.features - ): - collected_metrics = "weight data" - if "BLOOD_PRESSURE" in ARGS.features: - collected_metrics += " or blood pressure" - elif "diastolic_blood_pressure" in group_data: - collected_metrics += ", but blood pressure (to enable sync set --features BLOOD_PRESSURE)" - - logging.info( - "%s This Withings metric contains no %s. Not syncing...", - dt, - collected_metrics, - ) - groupdata_log_raw_data(group_data) - # Do not delete existing data for this timestamp; there may be valid - # weight or blood pressure measurements in other groups with the same timestamp. - # Simply skip this non-whitelisted group. - continue - - if height and "weight" in group_data: - group_data["bmi"] = round( - group_data["weight"] / pow(group_data["height"], 2), 1 - ) - if "hydration" in group_data and group_data["hydration"]: - group_data["percent_hydration"] = round( - group_data["hydration"] * 100.0 / group_data["weight"], 2 - ) - logging.info("%s This Withings metric contains valid data. Syncing...", dt) - logging.debug("%s Detected data: ", dt) - groupdata_log_raw_data(group_data) - if "weight" in group_data: - logging.debug( - "Record: %s, type=%s\n" - "height=%s m, " - "weight=%s kg, " - "fat_ratio=%s %%, " - "muscle_mass=%s kg, " - "percent_hydration=%s %%, " - "bone_mass=%s kg, " - "bmi=%s", - group_data["date_time"], - group_data["type"], - group_data["height"], - group_data["weight"], - group_data["fat_ratio"], - group_data["muscle_mass"], - group_data["percent_hydration"], - group_data["bone_mass"], - group_data["bmi"], - ) - if "diastolic_blood_pressure" in group_data: - logging.debug( - "Record: %s, type=%s\n" - "diastolic_blood_pressure=%s mmHg, " - "systolic_blood_pressure=%s mmHg, " - "heart_pulse=%s BPM, ", - group_data["date_time"], - group_data["type"], - group_data["diastolic_blood_pressure"], - group_data["systolic_blood_pressure"], - group_data["heart_pulse"], - ) - # join groups with same timestamp - # Merge without letting a later non-weight/bp group override an existing valid record - existing = sync_dict[dt] - # merge raw_data lists for richer JSON/debug output - if "raw_data" in group_data: - existing.setdefault("raw_data", []) - existing["raw_data"].extend(group_data["raw_data"]) +def interval_seconds(value: str) -> int: + match = re.fullmatch(r"\s*(\d+)\s*([smhd]?)\s*", value.lower()) + if not match or int(match.group(1)) <= 0: + raise argparse.ArgumentTypeError( + "SYNC_INTERVAL must be a positive number of seconds or use s, m, h, or d." + ) + factor = {"": 1, "s": 1, "m": 60, "h": 3600, "d": 86400}[match.group(2)] + return int(match.group(1)) * factor - # decide resulting type: prefer weight when present - if "type" not in existing or existing["type"] == "None": - existing["type"] = group_data["type"] - elif existing["type"] == "weight" and group_data["type"] != "weight": - # keep weight; do not downgrade to another type - pass - elif existing["type"] != "weight": - # allow switching from non-weight to the new type (e.g., blood_pressure) - existing["type"] = group_data["type"] - # merge scalar fields; keep existing non-None values - for k, v in group_data.items(): - if k in ("type", "raw_data"): +def parse_since(value: str) -> datetime: + try: + return datetime.combine( + datetime.strptime(value, "%Y-%m-%d").date(), time.min, timezone.utc + ) + except ValueError as exc: + raise argparse.ArgumentTypeError("Use YYYY-MM-DD for --since.") from exc + + +class SyncService: + def __init__(self, storage: Storage, withings: WithingsClient, garmin: GarminClient): + self.storage = storage + self.withings = withings + self.garmin = garmin + + def run_once(self, since: datetime | None = None) -> int: + state: dict[str, Any] = self.storage.read_json(STATE_FILE) + last_synced = state.get("last_synced_at") + if since is None: + if last_synced: + since = datetime.fromisoformat(last_synced) - timedelta(days=1) + else: + since = datetime(2009, 1, 1, tzinfo=timezone.utc) + end = datetime.now(timezone.utc) + groups = self.withings.get_measurements(since, end) + height = self.withings.get_height() + measurements = parse_measurement_groups(groups, height) + uploaded = state.setdefault("uploaded_measurements", {}) + if not isinstance(uploaded, dict): + raise ValueError("Invalid uploaded_measurements in sync state.") + count = 0 + for measurement in measurements: + if measurement.source_id in uploaded: continue - if v is not None: - existing[k] = v - - last_measurement_type = None - - # Iterate in chronological order for determinism - for dt in sorted(sync_dict.keys()): - group_data = sync_dict[dt] - # Skip empty or non-whitelisted groups (those that never collected a valid type) - if not group_data or "type" not in group_data or group_data["type"] == "None": - logging.debug("skipping data with timestamp: %s, type: %s", dt, group_data.get("type") if group_data else "empty") - if group_data: - # Log the group_data as JSON (excluding raw_data objects) - debug_data = {k: v for k, v in group_data.items() if k != "raw_data"} - logging.debug("skipped record details: %s", json.dumps(debug_data, indent=2, default=str)) - continue - syncdata.append(group_data) - logging.debug("Processed data: ") - for k, v in group_data.items(): - logging.debug("%s=%s", k, v) - if last_date_time is None or group_data["date_time"] > last_date_time: - last_date_time = group_data["date_time"] - last_measurement_type = group_data["type"] - logging.debug("last_dt: %s last_weight: %s", last_date_time, last_weight) - - if last_measurement_type is None: - logging.error("Invalid or no data detected") - - return last_measurement_type, last_date_time, syncdata - - -def groupdata_log_raw_data(groupdata): - """Logs raw data to debug""" - for dataentry in groupdata["raw_data"]: - logging.debug("%s", dataentry) - # Detailed structure to help map unknown fields - try: - logging.debug( - " -> type_id=%s label=%s unit_str=%s unit_exp=%s raw_value=%s human_value=%s", - getattr(dataentry, "type", None), - getattr(dataentry, "type_s", None), - getattr(dataentry, "unit_s", None), - getattr(dataentry, "unit", None), - getattr(dataentry, "value", None), - round(dataentry.get_value(), 6) if hasattr(dataentry, "get_value") else None, + self.garmin.upload(measurement) + uploaded[measurement.source_id] = measurement.timestamp.isoformat() + prior = state.get("last_synced_at") + if not prior or measurement.timestamp > datetime.fromisoformat(prior): + state["last_synced_at"] = measurement.timestamp.isoformat() + self.storage.write_json(STATE_FILE, state) + count += 1 + log.info( + "Synced %s kg from %s", + measurement.weight, + measurement.timestamp.isoformat(), ) - except Exception as e: - logging.debug(" -> failed to print detailed entry: %s", e) - - -def write_withings_raw_json(filename, raw_json): - """Write raw Withings JSON to file for debugging/mapping purposes""" - logging.info("Writing Withings raw JSON to %s.", filename) - try: - with open(filename, "w", encoding="utf-8") as jf: - json.dump(raw_json, jf, indent=2, default=str) - except OSError: - logging.error("Unable to open output jsonfile! %s", filename) - - -def write_to_fitfile(filename, fit_data): - """Writes fit data to fit file""" - logging.info("Writing fitfile to %s.", filename) - try: - with open(filename, "wb") as fitfile: - fitfile.write(fit_data.getvalue()) - except OSError: - logging.error("Unable to open output fitfile! %s", filename) - - -def write_to_file_when_needed(fit_data_weigth, fit_data_blood_pressure, json_data): - """Write measurements to file when requested""" - if ARGS.output is not None: - if ARGS.to_fit: - if fit_data_weigth is not None: - write_to_fitfile(ARGS.output + ".weight.fit", fit_data_weigth) - if fit_data_blood_pressure is not None: - write_to_fitfile( - ARGS.output + ".blood_pressure.fit", fit_data_blood_pressure - ) - - if ARGS.to_json: - filename = ARGS.output + ".json" - logging.info("Writing jsonfile to %s.", filename) - try: - with open(filename, "w", encoding="utf-8") as jsonfile: - json.dump(json_data, jsonfile, indent=4) - except OSError: - logging.error("Unable to open output jsonfile!") - - -def sync(): - """Sync measurements from Withings to Garmin a/o TrainerRoad""" - # Prepare config folder - config_folder = None - if ARGS.config_folder: - config_folder = os.path.abspath(os.path.expanduser(ARGS.config_folder)) - # Create directory if it doesn't exist - os.makedirs(config_folder, exist_ok=True) - - # Withings API - withings = WithingsAccount(config_folder=config_folder) - - if not ARGS.fromdate: - startdate = withings.get_lastsync() - else: - startdate = int(time.mktime(ARGS.fromdate.timetuple())) - - enddate = int(time.mktime(ARGS.todate.timetuple())) + 86399 - logging.info( - "Fetching measurements from %s to %s", - time.strftime("%Y-%m-%d %H:%M", time.localtime(startdate)), - time.strftime("%Y-%m-%d %H:%M", time.localtime(enddate)), - ) - - height = withings.get_height() - groups = withings.get_measurements(startdate=startdate, enddate=enddate) - - # Only upload if there are measurement returned - if groups is None or len(groups) == 0: - logging.error("No measurements to upload for date or period specified") - return -1 - - last_measurement_type, last_date_time, syncdata = prepare_syncdata(height, groups) - - # dump raw Withings JSON to a file - if ARGS.dump_raw and hasattr(withings, "last_measurements_json"): - if ARGS.output: - raw_filename = ARGS.output + ".withings_raw.json" - else: - # Create a default filename with date range - start_s = time.strftime("%Y%m%d", time.localtime(startdate)) - end_s = time.strftime("%Y%m%d", time.localtime(enddate)) - raw_filename = f"withings_raw_{start_s}_{end_s}.json" - write_withings_raw_json(raw_filename, withings.last_measurements_json) - - fit_data_weight, fit_data_blood_pressure = generate_fitdata(syncdata) - json_data = generate_jsondata(syncdata) - - write_to_file_when_needed(fit_data_weight, fit_data_blood_pressure, json_data) - - if not ARGS.no_upload: - # get weight entries (in case of only blood_pressure) - only_weight_entries = list(filter(lambda x: (x["type"] == "weight"), syncdata)) - last_weight_exists = len(only_weight_entries) > 0 - # Upload to Trainer Road - if ARGS.trainerroad_username and last_weight_exists: - # sort and get last weight - last_weight_measurement = sorted( - only_weight_entries, key=lambda x: x["date_time"] - )[-1] - last_weight = last_weight_measurement["weight"] - logging.info("Trainerroad username set -- attempting to sync") - logging.info(" Last weight %s", last_weight) - logging.info(" Measured %s", last_date_time) - if sync_trainerroad(last_weight): - logging.info("TrainerRoad update done!") - else: - logging.info("No TrainerRoad username or a new measurement - skipping sync") - - # Upload to Garmin Connect - if ARGS.garmin_username and ( - fit_data_weight is not None or fit_data_blood_pressure is not None - ): - logging.debug("attempting to upload fit file...") - gar_wg_state = None - gar_bp_state = None - if fit_data_weight is not None: - gar_wg_state = sync_garmin(fit_data_weight, config_folder) - if gar_wg_state: - logging.info( - "Fit file with weight information uploaded to Garmin Connect" - ) - if fit_data_blood_pressure is not None: - gar_bp_state = sync_garmin(fit_data_blood_pressure, config_folder) - if gar_bp_state: - logging.info( - "Fit file with blood pressure information uploaded to Garmin Connect" - ) - if gar_wg_state or gar_bp_state: - # Save this sync so we don't re-download the same data again (if no range has been specified) - if not ARGS.fromdate: - withings.set_lastsync() - elif ARGS.garmin_username is None: - logging.info("No Garmin username - skipping sync") - else: - logging.info("No Garmin data selected - skipping sync") - else: - logging.info("Skipping upload") - return 0 - - -ARGS = get_args() - + log.info("Sync complete: %d new measurement(s).", count) + return count + + def status(self, healthcheck: bool = False) -> int: + withings_ok, withings_expiry = self.withings.status() + garmin_ok, garmin_expiry = self.garmin.status() + print(f"Withings: {'tokens saved' if withings_ok else 'not authenticated'}" + f"{_expiry_text(withings_expiry)}") + print(f"Garmin: {'tokens saved' if garmin_ok else 'not authenticated'}" + f"{_expiry_text(garmin_expiry)}") + state = self.storage.read_json(STATE_FILE) + print(f"Last sync: {state.get('last_synced_at', 'never')}") + return 0 if (withings_ok and garmin_ok) else (1 if healthcheck else 0) + + +def _expiry_text(expiry: datetime | None) -> str: + return f" (access token expires {expiry.isoformat()})" if expiry else " (expiry unknown)" + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Sync Withings body composition to Garmin Connect.") + subparsers = parser.add_subparsers(dest="command") + auth = subparsers.add_parser("auth", help="Authenticate a service.") + auth_commands = auth.add_subparsers(dest="service", required=True) + auth_commands.add_parser("withings", help="Authorize a personal Withings app.") + garmin_auth = auth_commands.add_parser("garmin", help="Authenticate Garmin Connect, including MFA.") + garmin_auth.add_argument("--mfa-code", help="Supply an authenticator code without an interactive MFA prompt.") + sync = subparsers.add_parser("sync", help="Run a synchronization.") + sync.add_argument("--once", action="store_true", help="Perform one sync and exit.") + sync.add_argument("--since", type=parse_since, help="Backfill from YYYY-MM-DD.") + subparsers.add_parser("status", help="Show saved token and sync status.") + subparsers.choices["status"].add_argument("--healthcheck", action="store_true") + return parser + + +def _new_service(storage: Storage) -> SyncService: + return SyncService(storage, WithingsClient(storage), GarminClient(storage)) + + +def _authenticate_withings(client: WithingsClient) -> None: + state = new_oauth_state() + print("Open this URL and authorize access to your Withings measurements:") + print(client.authorization_url(state)) + print("Paste the full redirect URL or its authorization code.") + value = input("Redirect URL or code: ") + client.authenticate(value, state) + log.info("Withings authorization saved.") + + +def _scheduler(service: SyncService, storage: Storage, interval: int) -> None: + log.info("Starting sync scheduler (interval: %s seconds).", interval) + while True: + try: + with storage.lock(): + service.run_once() + except Exception: + log.exception("Scheduled sync failed; will retry after the next interval.") + clock.sleep(interval) -def main(): - """Main""" - if ARGS.verbose: - log_level = logging.DEBUG - elif ARGS.silent: - log_level = logging.WARNING - else: - log_level = logging.INFO +def main() -> None: logging.basicConfig( - level=log_level, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", - stream=sys.stdout, + level=os.getenv("LOG_LEVEL", "INFO").upper(), + format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) - logging.debug("withings-sync script version %s", version("withings-sync")) - logging.debug("Script invoked with the following arguments: %s", ARGS) - - if sys.version_info < (3, 12): - print("Sorry, requires at least Python3.12.") + parser = _build_parser() + args = parser.parse_args() + storage = Storage(os.getenv("DATA_DIR", "./data")) + try: + if args.command == "auth" and args.service == "withings": + with storage.lock(): + _authenticate_withings(WithingsClient(storage)) + elif args.command == "auth" and args.service == "garmin": + with storage.lock(): + GarminClient(storage).authenticate_interactively(args.mfa_code) + log.info("Garmin authentication saved.") + elif args.command == "sync": + service = _new_service(storage) + if args.once or args.since: + with storage.lock(): + service.run_once(args.since) + else: + _scheduler(service, storage, interval_seconds(os.getenv("SYNC_INTERVAL", "1h"))) + elif args.command == "status": + if args.healthcheck: + sys.exit(_new_service(storage).status(args.healthcheck)) + with storage.lock(): + sys.exit(_new_service(storage).status()) + else: + _scheduler(_new_service(storage), storage, interval_seconds(os.getenv("SYNC_INTERVAL", "1h"))) + except KeyboardInterrupt: + log.info("Stopped.") + except Exception as exc: + log.error("%s", exc) sys.exit(1) - sync() + +if __name__ == "__main__": + main() diff --git a/withings_sync/trainerroad.py b/withings_sync/trainerroad.py deleted file mode 100644 index c40daa4..0000000 --- a/withings_sync/trainerroad.py +++ /dev/null @@ -1,211 +0,0 @@ -import requests -import json -import logging - -logger = logging.getLogger(__name__) - - -class TrainerRoad: - _ftp = 'ftp' - _weight = 'weightKg' - _units_metric = 'kmh' - _units_imperial = 'mph' - _numerical_verify = (_ftp, _weight) - _login_url = 'https://www.trainerroad.com/app/login' - _logout_url = 'https://www.trainerroad.com/app/logout' - _profile_api_url = 'https://www.trainerroad.com/app/api/profile/rider-information' - _download_tcx_url = 'http://www.trainerroad.com/cycling/rides/download' - _workouts_url = 'https://api.trainerroad.com/api/careerworkouts' - - def __init__(self, username, password): - self._username = username - self._password = password - self._session = None - - def connect(self): - self._session = requests.Session() - - data = {'Username': self._username, - 'Password': self._password} - - r = self._session.post(self._login_url, data=data, - allow_redirects=False) - - if r.status_code not in [200, 302]: - raise RuntimeError("Error loging in to TrainerRoad (Code {})" - .format(r.status_code)) - - logger.info('Logged into TrainerRoad as "{}"'.format(self._username)) - - def disconnect(self): - r = self._session.get(self._logout_url, allow_redirects=False) - if r.status_code not in [200, 302]: - raise RuntimeError("Error loging out of TrainerRoad (Code {})" - .format(r.status_code)) - - self._session = None - logger.info('Logged out of TrainerRoad as "{}"'.format(self._username)) - - def __enter__(self): - self.connect() - return self - - def __exit__(self, exc_type, exc_value, exc_traceback): - self.disconnect() - - - - def _get(self, url): - if self._session is None: - raise RuntimeError('Not Connected') - - # Add browser-like headers for API calls (camelCase JSON format) - headers = { - 'Accept': 'application/json, text/plain, */*', - 'Referer': 'https://www.trainerroad.com/app/profile/rider-information', - 'trainerroad-jsonformat': 'camel-case', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0' - } - - r = self._session.get(url, headers=headers) - - if r.status_code != 200: - raise RuntimeError("Error getting info from TrainerRoad (Code {})" - .format(r.status_code)) - - return r - - def _post(self, url, data): - if self._session is None: - raise RuntimeError('Not Connected') - - r = self._session.post(url, data) - - if r.status_code != 200: - raise RuntimeError("Error posting info to TrainerRoad (Code {})" - .format(r.status_code)) - - return r - - def _read_profile(self): - """Read profile data from TrainerRoad JSON API""" - r = self._get(self._profile_api_url) - - if r.status_code != 200: - raise RuntimeError("Error getting profile from TrainerRoad (Code {})" - .format(r.status_code)) - - profile_data = r.json() - logger.debug("Profile API response: {}".format(json.dumps(profile_data, indent=2))) - - return profile_data - - def _write_profile(self, new_values): - """Write profile data to TrainerRoad JSON API""" - # Read current values - data = self._read_profile() - original_data = data.copy() - - logger.info("Current profile values: Weight={}, FTP={}".format( - data.get(self._weight), data.get(self._ftp))) - - # Update values with new_values - for key, value in new_values.items(): - if key == self._weight and data.get('Units') == self._units_imperial: - value = round(value/0.45359237, 1) - logger.debug("Converting Weight to lbs {}".format(value)) - - data[key] = value - - logger.info("Updating profile: Weight={}, FTP={}".format( - data.get(self._weight), data.get(self._ftp))) - - # Send PUT request with JSON data (exact browser headers) - headers = { - 'Accept': 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - 'Origin': 'https://www.trainerroad.com', - 'Referer': 'https://www.trainerroad.com/app/profile/rider-information', - 'trainerroad-jsonformat': 'camel-case', - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0' - } - r = self._session.put(self._profile_api_url, json=data, headers=headers) - - logger.debug("PUT request status: {}, response: {}".format(r.status_code, r.text[:200])) - - if r.status_code not in [200, 204]: - raise RuntimeError("Error updating TrainerRoad profile (Code {})".format(r.status_code)) - - # Verify the changes - updated_data = self._read_profile() - logger.info("Profile updated successfully: Weight={}, FTP={}".format( - updated_data.get(self._weight), updated_data.get(self._ftp))) - - # Verify numerical fields - for key in self._numerical_verify: - if key in new_values: - expected = float(new_values[key]) - if key == self._weight and original_data.get('Units') == self._units_imperial: - expected = round(expected/0.45359237, 1) - actual = float(updated_data.get(key, 0)) - if abs(expected - actual) > 0.1: # Allow small rounding differences - raise RuntimeError('Failed to verify numerical key {}: expected {}, got {}' - .format(key, expected, actual)) - - return - - @property - def ftp(self): - """Get current FTP value from TrainerRoad""" - values = self._read_profile() - return values.get(self._ftp) - - @ftp.setter - def ftp(self, value): - """Set FTP value in TrainerRoad""" - self._write_profile({self._ftp: value}) - - @property - def weight(self): - """Get current weight value from TrainerRoad""" - values = self._read_profile() - return values.get(self._weight) - - @weight.setter - def weight(self, value): - """Set weight value in TrainerRoad""" - self._write_profile({self._weight: value}) - - def download_tcx(self, id): - res = self._session.get('{}/{}'.format(self._download_tcx_url, str(id))) - if res.status_code != 200: - raise RuntimeError("Unable to download (code = {})" - .format(res.status_code)) - - return res.text - - def get_workouts(self): - res = self._session.get(self._workouts_url) - if res.status_code != 200: - raise RuntimeError("Unable to download (code = {})" - .format(res.status_code)) - - data = json.loads(res.text) - logger.debug(json.dumps(data, indent=4, sort_keys=True)) - - logger.info('Recieved info on {} workouts'.format(len(data))) - - return data - - def get_workout(self, guid): - res = self._session.get(self._workout_url - + '?guid={}'.format(str(guid))) - - if res.status_code != 200: - raise RuntimeError('Unable to get workout "{}" (Code = {})' - .format(guid, res.status_code)) - - data = json.loads(res.text) - logger.debug(json.dumps(data, indent=4, sort_keys=True)) - - return data diff --git a/withings_sync/withings.py b/withings_sync/withings.py new file mode 100644 index 0000000..429ff87 --- /dev/null +++ b/withings_sync/withings.py @@ -0,0 +1,278 @@ +"""Withings OAuth and weight-measurement API.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone +import hashlib +import json +import math +import os +from typing import Any +from urllib.parse import parse_qs, urlencode, urlparse +import uuid + +import requests + +from withings_sync.storage import Storage + +AUTHORIZE_URL = "https://account.withings.com/oauth2_user/authorize2" +TOKEN_URL = "https://wbsapi.withings.net/v2/oauth2" +MEASURE_URL = "https://wbsapi.withings.net/measure" +TOKEN_FILE = "withings.json" + + +class WithingsError(RuntimeError): + """Raised when a Withings operation cannot complete.""" + + +@dataclass(frozen=True) +class Measurement: + source_id: str + timestamp: datetime + weight: float + percent_fat: float | None + percent_hydration: float | None + bone_mass: float | None + muscle_mass: float | None + bmi: float | None + visceral_fat: float | None = None + + +def _scaled_value(measure: dict[str, Any]) -> float | None: + value = measure.get("value") + unit = measure.get("unit", 0) + if value is None: + return None + try: + result = float(value) * (10 ** int(unit)) + if not math.isfinite(result): + raise ValueError("measurement must be finite") + return round(result, 2) + except (TypeError, ValueError, OverflowError) as exc: + raise WithingsError(f"Invalid measurement value: {measure!r}") from exc + + +def parse_measurement_groups( + groups: list[dict[str, Any]], height_m: float | None = None +) -> list[Measurement]: + result = [] + for group in groups: + try: + timestamp = datetime.fromtimestamp(int(group["date"]), timezone.utc) + measures = { + int(item["type"]): _scaled_value(item) + for item in group.get("measures", []) + } + except (KeyError, TypeError, ValueError, OverflowError) as exc: + raise WithingsError(f"Invalid Withings measurement group: {group!r}") from exc + weight = measures.get(1) + if weight is None or weight <= 0: + continue + bmi = ( + round(weight / (height_m**2), 1) + if height_m and math.isfinite(height_m) and height_m > 0 + else None + ) + hydration = measures.get(77) + result.append( + Measurement( + source_id=str( + group.get("grpid") + or hashlib.sha256( + json.dumps(group, sort_keys=True).encode("utf-8") + ).hexdigest() + ), + timestamp=timestamp, + weight=weight, + percent_fat=measures.get(6), + percent_hydration=( + round(hydration * 100 / weight, 2) if hydration is not None else None + ), + bone_mass=measures.get(88), + muscle_mass=measures.get(76), + bmi=bmi, + visceral_fat=measures.get(170), + ) + ) + return sorted(result, key=lambda measure: (measure.timestamp, measure.source_id)) + + +class WithingsClient: + def __init__( + self, + storage: Storage, + client_id: str | None = None, + client_secret: str | None = None, + redirect_uri: str | None = None, + session: requests.Session | None = None, + ): + self.storage = storage + self.client_id = client_id or os.getenv("WITHINGS_CLIENT_ID", "") + self.client_secret = client_secret or os.getenv("WITHINGS_CLIENT_SECRET", "") + self.redirect_uri = redirect_uri or os.getenv("WITHINGS_REDIRECT_URI", "") + self.session = session or requests.Session() + + def _require_app(self) -> None: + if not all((self.client_id, self.client_secret, self.redirect_uri)): + raise WithingsError( + "Set WITHINGS_CLIENT_ID, WITHINGS_CLIENT_SECRET, and " + "WITHINGS_REDIRECT_URI in .env before authenticating." + ) + + def authorization_url(self, state: str) -> str: + self._require_app() + return f"{AUTHORIZE_URL}?{urlencode({'response_type': 'code', 'client_id': self.client_id, 'state': state, 'scope': 'user.metrics', 'redirect_uri': self.redirect_uri})}" + + @staticmethod + def extract_code(value: str, expected_state: str | None = None) -> str: + value = value.strip() + if value.startswith(("http://", "https://")): + query = parse_qs(urlparse(value).query) + code = query.get("code", [""])[0] + returned_state = query.get("state", [None])[0] + if expected_state and returned_state != expected_state: + raise WithingsError("The redirect URL state did not match this login.") + else: + code = value + if not code: + raise WithingsError("No authorization code was found in the supplied value.") + return code + + def exchange_code(self, code: str) -> None: + self._require_app() + response = self.session.post( + TOKEN_URL, + data={ + "action": "requesttoken", + "grant_type": "authorization_code", + "client_id": self.client_id, + "client_secret": self.client_secret, + "code": code, + "redirect_uri": self.redirect_uri, + }, + timeout=30, + ) + response.raise_for_status() + payload = response.json() + self._check_response(payload, "authorization code exchange") + self._save_tokens(payload["body"]) + + def _check_response(self, payload: dict[str, Any], action: str) -> None: + if payload.get("status") != 0: + raise WithingsError( + f"Withings {action} failed (status {payload.get('status')}): " + f"{payload.get('error') or payload.get('body') or 'unknown API error'}" + ) + + def _save_tokens(self, tokens: dict[str, Any]) -> None: + try: + access_token = tokens["access_token"] + refresh_token = tokens["refresh_token"] + except KeyError as exc: + raise WithingsError("Withings token response omitted a required token.") from exc + expires_in = int(tokens.get("expires_in", 10800)) + self.storage.write_json( + TOKEN_FILE, + { + "access_token": access_token, + "refresh_token": refresh_token, + "userid": tokens.get("userid"), + "expires_at": int(datetime.now(timezone.utc).timestamp()) + expires_in, + }, + ) + + def _tokens(self) -> dict[str, Any]: + return self.storage.read_json(TOKEN_FILE) + + def access_token(self) -> str: + self._require_app() + tokens = self._tokens() + if not tokens.get("refresh_token"): + raise WithingsError("Withings is not authenticated. Run: docker compose run --rm withings-sync auth withings") + if int(tokens.get("expires_at", 0)) <= int(datetime.now(timezone.utc).timestamp()) + 300: + response = self.session.post( + TOKEN_URL, + data={ + "action": "requesttoken", + "grant_type": "refresh_token", + "client_id": self.client_id, + "client_secret": self.client_secret, + "refresh_token": tokens["refresh_token"], + }, + timeout=30, + ) + response.raise_for_status() + payload = response.json() + self._check_response(payload, "token refresh") + self._save_tokens(payload["body"]) + tokens = self._tokens() + return str(tokens["access_token"]) + + def get_measurements(self, start: datetime, end: datetime) -> list[dict[str, Any]]: + token = self.access_token() + groups: list[dict[str, Any]] = [] + offset = 0 + while True: + response = self.session.post( + MEASURE_URL, + params={"action": "getmeas"}, + data={ + "access_token": token, + "category": 1, + "startdate": int(start.timestamp()), + "enddate": int(end.timestamp()), + "offset": offset, + }, + timeout=30, + ) + response.raise_for_status() + payload = response.json() + self._check_response(payload, "measurement request") + body = payload.get("body") or {} + page = body.get("measuregrps") or [] + groups.extend(page) + if not body.get("more") or not page: + break + next_offset = int(body.get("offset", offset + len(page))) + if next_offset <= offset: + raise WithingsError("Withings pagination did not advance its offset.") + offset = next_offset + return groups + + def get_height(self) -> float | None: + token = self.access_token() + response = self.session.post( + MEASURE_URL, + params={"action": "getmeas"}, + data={"access_token": token, "meastype": 4, "category": 1}, + timeout=30, + ) + response.raise_for_status() + payload = response.json() + self._check_response(payload, "height request") + groups = (payload.get("body") or {}).get("measuregrps") or [] + heights = [ + (int(group.get("date", 0)), _scaled_value(measure)) + for group in groups + for measure in group.get("measures", []) + if int(measure.get("type", -1)) == 4 and _scaled_value(measure) is not None + ] + return max(heights, default=(0, None), key=lambda item: item[0])[1] + + def authenticate(self, value: str, expected_state: str | None = None) -> None: + self.exchange_code(self.extract_code(value, expected_state)) + + def status(self) -> tuple[bool, datetime | None]: + tokens = self._tokens() + if not tokens.get("refresh_token"): + return False, None + expiry = tokens.get("expires_at") + return ( + True, + datetime.fromtimestamp(int(expiry), timezone.utc) if expiry else None, + ) + + +def new_oauth_state() -> str: + return uuid.uuid4().hex diff --git a/withings_sync/withings2.py b/withings_sync/withings2.py deleted file mode 100644 index 3f44791..0000000 --- a/withings_sync/withings2.py +++ /dev/null @@ -1,529 +0,0 @@ -"""This module takes care of the communication with Withings.""" -from datetime import date, datetime -import logging -import json -import os -import time -import importlib_resources -import requests - -log = logging.getLogger("withings") - -HOME = os.environ.get("HOME", ".") -AUTHORIZE_URL = "https://account.withings.com/oauth2_user/authorize2" -TOKEN_URL = "https://wbsapi.withings.net/v2/oauth2" -GETMEAS_URL = "https://wbsapi.withings.net/measure?action=getmeas" - - -APP_CONFIG = os.environ.get( - "WITHINGS_APP", - importlib_resources.files(__name__) / "config/withings_app.json", -) -USER_CONFIG = os.environ.get("WITHINGS_USER", HOME + "/.withings_user.json") - - -class WithingsException(Exception): - """Pass WithingsExceptions""" - - -class WithingsConfig: - """This class takes care of the Withings config file""" - - config = {} - config_file = "" - - def __init__(self, config_file): - self.config_file = config_file - self.read() - - def read(self): - """reads config file""" - try: - with open(self.config_file, encoding="utf8") as configfile: - self.config = json.load(configfile) - except (ValueError, FileNotFoundError): - log.error("Can't read config file %s", self.config_file) - self.config = {} - - def write(self): - """writes config file""" - with open(self.config_file, "w", encoding="utf8") as configfile: - json.dump(self.config, configfile, indent=4, sort_keys=True) - - -class WithingsOAuth2: - """This class takes care of the Withings OAuth2 authentication""" - - app_config = user_config = None - - def __init__(self, config_folder=None): - # Determine app config file path with fallback chain - if config_folder: - app_config_path = os.path.join(config_folder, "withings_app.json") - if os.path.exists(app_config_path): - log.info(f"Using app config from: {app_config_path}") - else: - log.warning(f"App config not found at {app_config_path}, falling back to WITHINGS_APP env var or default") - app_config_path = os.environ.get("WITHINGS_APP", APP_CONFIG) - else: - app_config_path = os.environ.get("WITHINGS_APP", APP_CONFIG) - - log.info(f"Loading app config from: {app_config_path}") - app_cfg = WithingsConfig(app_config_path) - self.app_config = app_cfg.config - - # Determine user config file path - if config_folder: - user_config_path = os.path.join(config_folder, ".withings_user.json") - # Always use the new path when config_folder is specified - # The directory will be created when needed - else: - user_config_path = USER_CONFIG - - self.user_cfg = WithingsConfig(user_config_path) - self.user_config = self.user_cfg.config - - if config_folder and not os.path.exists(user_config_path): - legacy_path = HOME + "/.withings_user.json" - if os.path.exists(legacy_path): - log.info(f"Using new config folder: {user_config_path}") - log.info(f"If you want to use existing credentials, copy from: {legacy_path}") - - if not self.user_config.get("access_token"): - if not self.user_config.get("authentification_code"): - self.user_config[ - "authentification_code" - ] = self.get_authenticationcode() - try: - self.get_accesstoken() - except Exception as e: - log.warning("Could not get access-token. Trying to renew auth_code") - self.user_config[ - "authentification_code" - ] = self.get_authenticationcode() - self.get_accesstoken() - - - self.refresh_accesstoken() - - self.user_cfg.write() - - def update_config(self): - """updates config file""" - self.user_cfg.write() - - def get_authenticationcode(self): - """get Withings authentication code""" - params = { - "response_type": "code", - "client_id": self.app_config["client_id"], - "state": "OK", - "scope": "user.metrics", - "redirect_uri": self.app_config["callback_url"], - } - - log.warning( - "User interaction needed to get Authentification " "Code from Withings!" - ) - log.warning("") - log.warning( - "Open the following URL in your web browser and copy back " - "the token. You will have *30 seconds* before the " - "token expires. HURRY UP!" - ) - log.warning("(This is one-time activity)") - log.warning("") - - url = AUTHORIZE_URL + "?" - - for key, value in params.items(): - url = url + key + "=" + value + "&" - - log.warning(url) - log.warning("") - - authentification_code = input("Token : ") - - return authentification_code - - def get_accesstoken(self): - """get Withings access token""" - log.info("Get Access Token") - - params = { - "action": "requesttoken", - "grant_type": "authorization_code", - "client_id": self.app_config["client_id"], - "client_secret": self.app_config["consumer_secret"], - "code": self.user_config["authentification_code"], - "redirect_uri": self.app_config["callback_url"], - } - - req = requests.post(TOKEN_URL, params) - resp = req.json() - - status = resp.get("status") - body = resp.get("body") - - if status != 0: - log.error("Received error code: %d", status) - log.error( - "Check here for an interpretation of this error: " - "http://developer.withings.com/api-reference#section/Response-status" - ) - log.error("") - log.error( - "If it's regarding an invalid code, try to start the" - " script again to obtain a new link." - ) - raise - - self.user_config["access_token"] = body.get("access_token") - self.user_config["refresh_token"] = body.get("refresh_token") - self.user_config["userid"] = body.get("userid") - - def refresh_accesstoken(self): - """refresh Withings access token""" - log.info("Refresh Access Token") - - params = { - "action": "requesttoken", - "grant_type": "refresh_token", - "client_id": self.app_config["client_id"], - "client_secret": self.app_config["consumer_secret"], - "refresh_token": self.user_config["refresh_token"], - } - - req = requests.post(TOKEN_URL, params) - resp = req.json() - - status = resp.get("status") - body = resp.get("body") - - if status != 0: - log.error("Received error code: %d", status) - log.error( - "Check here for an interpretation of this error: " - "http://developer.withings.com/api-reference#section/Response-status" - ) - log.error("") - log.error( - "If it's regarding an invalid code, try to start the" - " script again to obtain a new link." - ) - - self.user_config["access_token"] = body.get("access_token") - self.user_config["refresh_token"] = body.get("refresh_token") - self.user_config["userid"] = body.get("userid") - - -class WithingsAccount: - """This class gets measurements from Withings""" - - def __init__(self, config_folder=None): - self.withings = WithingsOAuth2(config_folder=config_folder) - - def get_lastsync(self): - """get last sync timestamp""" - if not self.withings.user_config.get("last_sync"): - return int(time.mktime(date.today().timetuple())) - return self.withings.user_config["last_sync"] - - def set_lastsync(self): - """set last sync timestamp""" - self.withings.user_config["last_sync"] = int(time.time()) - log.info("Saving Last Sync") - self.withings.update_config() - - def get_measurements(self, startdate, enddate): - """get Withings measurements""" - log.info("Get Measurements") - - params = { - "access_token": self.withings.user_config["access_token"], - # 'meastype': MEASTYPE_WEIGHT, - "category": 1, - "startdate": startdate, - "enddate": enddate, - } - - req = requests.post(GETMEAS_URL, params) - - measurements = req.json() - - if measurements.get("status") == 0: - log.debug("Measurements received") - return [ - WithingsMeasureGroup(g) - for g in measurements.get("body").get("measuregrps") - ] - return None - - def get_height(self): - """get height from Withings""" - height = None - height_timestamp = None - height_group = None - - log.debug("Get Height") - - params = { - "access_token": self.withings.user_config["access_token"], - "meastype": WithingsMeasure.TYPE_HEIGHT, - "category": 1, - } - - req = requests.post(GETMEAS_URL, params) - - measurements = req.json() - - if measurements.get("status") == 0: - log.debug("Height received") - - # there could be multiple height records. use the latest one - for record in measurements.get("body").get("measuregrps"): - height_group = WithingsMeasureGroup(record) - if height is not None: - if height_timestamp is not None: - if height_group.get_datetime() > height_timestamp: - height = height_group.get_height() - else: - height = height_group.get_height() - height_timestamp = height_group.get_datetime() - - return height - - -class WithingsMeasureGroup: - """This class takes care of the group measurement functions""" - - def __init__(self, measuregrp): - self._raw_data = measuregrp - self.grpid = measuregrp.get("grpid") - self.attrib = measuregrp.get("attrib") - self.date = measuregrp.get("date") - self.category = measuregrp.get("category") - self.measures = [WithingsMeasure(m) for m in measuregrp["measures"]] - - def __iter__(self): - for measure in self.measures: - yield measure - - def __len__(self): - return len(self.measures) - - def get_datetime(self): - """convenient function to get date & time""" - return datetime.fromtimestamp(self.date) - - def get_raw_data(self): - """convenient function to get raw data""" - return self.measures - - def get_weight(self): - """convenient function to get weight""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_WEIGHT: - return round(measure.get_value(), 2) - return None - - def get_height(self): - """convenient function to get height""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_HEIGHT: - return round(measure.get_value(), 2) - return None - - def get_fat_free_mass(self): - """convenient function to get fat free mass""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_FAT_FREE_MASS: - return round(measure.get_value(), 2) - return None - - def get_fat_ratio(self): - """convenient function to get fat ratio""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_FAT_RATIO: - return round(measure.get_value(), 2) - return None - - def get_fat_mass_weight(self): - """convenient function to get fat mass weight""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_FAT_MASS_WEIGHT: - return round(measure.get_value(), 2) - return None - - def get_diastolic_blood_pressure(self): - """convenient function to get diastolic blood pressure""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_DIASTOLIC_BLOOD_PRESSURE: - return round(measure.get_value(), 2) - return None - - def get_systolic_blood_pressure(self): - """convenient function to get systolic blood pressure""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_SYSTOLIC_BLOOD_PRESSURE: - return round(measure.get_value(), 2) - return None - - def get_heart_pulse(self): - """convenient function to get heart pulse""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_HEART_PULSE: - return round(measure.get_value(), 2) - return None - - def get_temperature(self): - """convenient function to get temperature""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_TEMPERATURE: - return round(measure.get_value(), 2) - return None - - def get_sp02(self): - """convenient function to get sp02""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_SP02: - return round(measure.get_value(), 2) - return None - - def get_body_temperature(self): - """convenient function to get body temperature""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_BODY_TEMPERATURE: - return round(measure.get_value(), 2) - return None - - def get_skin_temperature(self): - """convenient function to get skin temperature""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_SKIN_TEMPERATURE: - return round(measure.get_value(), 2) - return None - - def get_muscle_mass(self): - """convenient function to get muscle mass""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_MUSCLE_MASS: - return round(measure.get_value(), 2) - return None - - def get_hydration(self): - """convenient function to get hydration""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_HYDRATION: - return round(measure.get_value(), 2) - return None - - def get_bone_mass(self): - """convenient function to get bone mass""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_BONE_MASS: - return round(measure.get_value(), 2) - return None - - def get_pulse_wave_velocity(self): - """convenient function to get pulse wave velocity""" - for measure in self.measures: - if measure.type == WithingsMeasure.TYPE_PULSE_WAVE_VELOCITY: - return round(measure.get_value(), 2) - return None - - -class WithingsMeasure: - """This class takes care of the individual measurements""" - - TYPE_WEIGHT = 1 - TYPE_HEIGHT = 4 - TYPE_FAT_FREE_MASS = 5 - TYPE_FAT_RATIO = 6 - TYPE_FAT_MASS_WEIGHT = 8 - TYPE_DIASTOLIC_BLOOD_PRESSURE = 9 - TYPE_SYSTOLIC_BLOOD_PRESSURE = 10 - TYPE_HEART_PULSE = 11 - TYPE_TEMPERATURE = 12 - TYPE_SP02 = 54 - TYPE_BODY_TEMPERATURE = 71 - TYPE_SKIN_TEMPERATURE = 73 - TYPE_MUSCLE_MASS = 76 - TYPE_HYDRATION = 77 - TYPE_BONE_MASS = 88 - TYPE_PULSE_WAVE_VELOCITY = 91 - TYPE_VO2MAX = 123 - TYPE_QRS_INTERVAL = 135 - TYPE_PR_INTERVAL = 136 - TYPE_QT_INTERVAL = 137 - TYPE_CORRECTED_QT_INTERVAL = 138 - TYPE_ATRIAL_FIBRILLATION_PPG = 139 - TYPE_FAT_MASS_SEGMENTS = 174 - TYPE_EXTRACELLULAR_WATER = 168 - TYPE_INTRACELLULAR_WATER = 169 - TYPE_VISCERAL_FAT = 170 - TYPE_MUSCLE_MASS_SEGMENTS = 175 - TYPE_VASCULAR_AGE = 155 - TYPE_ATRIAL_FIBRILLATION = 130 - TYPE_NERVE_HEALTH_LEFT_FOOT = 158 - TYPE_NERVE_HEALTH_RIGHT_FOOT = 159 - TYPE_NERVE_HEALTH_FEET = 167 - TYPE_ELECTRODERMAL_ACTIVITY_FEET = 196 - TYPE_ELECTRODERMAL_ACTIVITY_LEFT_FOOT = 197 - TYPE_ELECTRODERMAL_ACTIVITY_RIGHT_FOOT = 198 - - withings_table = { - TYPE_WEIGHT: ["Weight", "kg"], - TYPE_HEIGHT: ["Height", "meter"], - TYPE_FAT_FREE_MASS: ["Fat Free Mass", "kg"], - TYPE_FAT_RATIO: ["Fat Ratio", "%"], - TYPE_FAT_MASS_WEIGHT: ["Fat Mass Weight", "kg"], - TYPE_DIASTOLIC_BLOOD_PRESSURE: ["Diastolic Blood Pressure", "mmHg"], - TYPE_SYSTOLIC_BLOOD_PRESSURE: ["Systolic Blood Pressure", "mmHg"], - TYPE_HEART_PULSE: ["Heart Pulse", "bpm"], - TYPE_TEMPERATURE: ["Temperature", "celsius"], - TYPE_SP02: ["SP02", "%"], - TYPE_BODY_TEMPERATURE: ["Body Temperature", "celsius"], - TYPE_SKIN_TEMPERATURE: ["Skin Temperature", "celsius"], - TYPE_MUSCLE_MASS: ["Muscle Mass", "kg"], - TYPE_HYDRATION: ["Hydration", "kg"], - TYPE_BONE_MASS: ["Bone Mass", "kg"], - TYPE_PULSE_WAVE_VELOCITY: ["Pulse Wave Velocity", "m/s"], - TYPE_VO2MAX: ["VO2 max", "ml/min/kg"], - TYPE_QRS_INTERVAL: ["QRS interval duration based on ECG signal", "ms"], - TYPE_PR_INTERVAL: ["PR interval duration based on ECG signal", "ms"], - TYPE_QT_INTERVAL: ["QT interval duration based on ECG signal", "ms"], - TYPE_CORRECTED_QT_INTERVAL: ["Corrected QT interval duration based on ECG signal", "ms"], - TYPE_ATRIAL_FIBRILLATION_PPG: ["Atrial fibrillation result from PPG", "ms"], - TYPE_FAT_MASS_SEGMENTS: ["Fat Mass for segments in mass unit", "kg"], - TYPE_EXTRACELLULAR_WATER: ["Extracellular Water", "kg"], - TYPE_INTRACELLULAR_WATER: ["Intracellular Water", "kg"], - TYPE_VISCERAL_FAT: ["Extracellular Water", "kg"], - TYPE_MUSCLE_MASS_SEGMENTS: ["Muscle Mass for segments in mass unit", "kg"], - TYPE_VASCULAR_AGE: ["Vascular age", "years"], - TYPE_ATRIAL_FIBRILLATION: ["Atrial fibrillation result", "ms"], - TYPE_NERVE_HEALTH_LEFT_FOOT: ["Nerve Health Score left foot", ""], - TYPE_NERVE_HEALTH_RIGHT_FOOT: ["Nerve Health Score right foot", ""], - TYPE_NERVE_HEALTH_FEET: ["Nerve Health Score feet", ""], - TYPE_ELECTRODERMAL_ACTIVITY_FEET: ["Electrodermal activity feet", ""], - TYPE_ELECTRODERMAL_ACTIVITY_LEFT_FOOT: ["Electrodermal activity left foot", ""], - TYPE_ELECTRODERMAL_ACTIVITY_RIGHT_FOOT: ["Electrodermal activity right foot", ""], - } - - def __init__(self, measure): - self._raw_data = measure - self.value = measure.get("value") - self.type = measure.get("type") - self.unit = measure.get("unit") - self.type_s = self.withings_table.get(self.type, ["unknown", ""])[0] - self.unit_s = self.withings_table.get(self.type, ["unknown", ""])[1] - - def __str__(self): - return f"{self.type_s}: {self.get_value()} {self.unit_s}" - - def json_dict(self): - return { f"{self.type_s.replace(' ','_')}": { "Value": round(self.get_value(), 2), "Unit": f'{self.unit_s}'}} - - def get_value(self): - """get value""" - return self.value * pow(10, self.unit)