Repository: santoshgdev/fitnessllm-dataplatform
Pull request #24: Hotfix/FIT-00-fix-cicd
Author: login: santoshgdev
Created: 2025-05-09T01:38:25Z | Merged: 2025-05-09T02:28:31Z



Diff:
diff --git a/.codecov.yaml b/.codecov.yaml
new file mode 100644
index 00000000..3bb75bc1
--- /dev/null
+++ b/.codecov.yaml
@@ -0,0 +1,8 @@
+---
+coverage:
+    status:
+        project:
+            default:
+                target: auto
+                threshold: 0%
+                base: auto
diff --git a/.github/actions/copy-shared/action.yaml b/.github/actions/copy-shared/action.yaml
new file mode 100644
index 00000000..b294aef2
--- /dev/null
+++ b/.github/actions/copy-shared/action.yaml
@@ -0,0 +1,15 @@
+---
+name: Copy Shared Code
+description: Copies shared code into each function directory
+runs:
+    using: composite
+    steps:
+        - run: |
+              for fn in cloud_functions/*; do
+                if [ -d "$fn" ] && [ "$(basename $fn)" != "shared" ]; then
+                  rm -rf "$fn/shared"
+                  mkdir -p "$fn/shared"
+                  cp -r cloud_functions/shared/. "$fn/shared/"
+                fi
+              done
+          shell: bash
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
deleted file mode 100644
index 4741733a..00000000
--- a/.github/workflows/ci.yaml
+++ /dev/null
@@ -1,39 +0,0 @@
----
-name: CI Checks
-
-on:
-    push:
-        branches:
-            - '*FIT-**'
-
-jobs:
-    quality-checks:
-        runs-on: ubuntu-latest
-        env:
-            PYTHONPATH: ${{ github.workspace }}
-
-        steps:
-            - uses: actions/checkout@v4
-
-            - name: Set up Python
-              uses: actions/setup-python@v4
-              with:
-                  python-version: 3.12.2
-                  cache: pip
-
-            - name: Cache pre-commit
-              uses: actions/cache@v4
-              with:
-                  path: ~/.cache/pre-commit
-                  key: precommit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }}
-
-            - name: Install dependencies
-              run: |
-                  pip install -e .
-                  pip install pre-commit pytest
-
-            - name: Run pre-commit
-              run: pre-commit run --show-diff-on-failure --all-files
-
-            - name: Run pytest
-              run: pytest tests/
diff --git a/.github/workflows/dev-deploy.yaml b/.github/workflows/dev-deploy.yaml
index 7c577c34..12a47916 100644
--- a/.github/workflows/dev-deploy.yaml
+++ b/.github/workflows/dev-deploy.yaml
@@ -18,46 +18,17 @@ env:
     REGION: us-west1
     SERVICE_NAME: fitnessllm-dp
     ENVIRONMENT: dev
-    PYTHONPATH: ${{ github.workspace }}
     PROJECT_ID: ${{ secrets.PROJECT_ID }}
     ENCRYPTION_SECRET: ${{ secrets.ENCRYPTION_SECRET }}
     STRAVA_SECRET: ${{ secrets.STRAVA_SECRET }}
     INFRASTRUCTURE_SECRET: ${{ secrets.INFRASTRUCTURE_SECRET }}
+    PYTHONPATH: ${{ github.workspace }}
 
 jobs:
-    check_changes:
+    testing:
         runs-on: ubuntu-latest
-        outputs:
-            changes_detected: ${{ steps.check_changes.outputs.changes_detected }}
         steps:
-            - name: Checkout code
-              uses: actions/checkout@v4
-              with:
-                  fetch-depth: 0
-
-            - name: Check for relevant file changes
-              id: check_changes
-              run: |
-                  git diff --name-only ${{ github.event.before }} ${{ github.sha }} > changed_files.txt
-                  echo "Changed files:"
-                  cat changed_files.txt
-
-                  if grep -q -E '^(fitnessllm_dataplatform/|Dockerfile|pyproject.toml|poetry.lock)' changed_files.txt; then
-                    echo "changes_detected=true" >> $GITHUB_OUTPUT
-                  else
-                    echo "changes_detected=false" >> $GITHUB_OUTPUT
-                  fi
-
-    cloud_run_deploy:
-        needs: check_changes
-        if: needs.check_changes.outputs.changes_detected == 'true' || (github.event_name == 'workflow_dispatch' && github.event.inputs.force_deploy == 'true')
-        runs-on: ubuntu-latest
-        environment: dev
-        steps:
-            - name: Checkout code
-              uses: actions/checkout@v4
-              with:
-                  fetch-depth: 3
+            - uses: actions/checkout@v4
 
             - name: Set up Python
               uses: actions/setup-python@v4
@@ -71,6 +42,14 @@ jobs:
                   virtualenvs-create: true
                   virtualenvs-in-project: true
 
+            - name: Cache Poetry virtualenv
+              uses: actions/cache@v4
+              with:
+                  path: .venv
+                  key: venv-${{ runner.os }}-${{ hashFiles('poetry.lock') }}
+                  restore-keys: |
+                      venv-${{ runner.os }}-
+
             - name: Update Poetry lock file
               run: |
                   poetry lock
@@ -79,11 +58,7 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
@@ -91,28 +66,78 @@ jobs:
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-
+
+            - name: Install pre-commit
+              run: pip install pre-commit==3.5.0
+
+            - name: Run pre-commit checks
+              run: pre-commit run --all-files
+
+            - name: Copy shared code into each function directory
+              uses: ./.github/actions/copy-shared
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Run tests with coverage
               run: |
-                  pip install pre-commit
-                  pre-commit install
-                  # Run once to populate the cache
-                  pre-commit run --all-files || true
-
-            - name: Save pre-commit cache
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
-              uses: actions/cache/save@v4
+                  PYTHONPATH=$PWD poetry run pytest tests/ \
+                    --cov-branch \
+                    --cov=cloud_functions \
+                    --cov-report=xml \
+                    --cov-report=term \
+                    --junitxml=junit.xml \
+                    -o junit_family=legacy
+
+            - name: Upload coverage to Codecov
+              uses: codecov/codecov-action@v4
               with:
-                  path: |
-                      ~/.cache/pre-commit
-                      ~/.cache/pip
-                  key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
+                  files: ./coverage.xml
+                  fail_ci_if_error: false
+                  token: ${{ secrets.CODECOV_TOKEN }}
 
-            - name: Run pre-commit checks
+            - name: Upload test results to Codecov
+              if: ${{ !cancelled() }}
+              uses: codecov/test-results-action@v1
+              with:
+                  token: ${{ secrets.CODECOV_TOKEN }}
+
+    cloud_run_job_detect_changes:
+        needs: testing
+        runs-on: ubuntu-latest
+        outputs:
+            changes_detected: ${{ steps.check_changes.outputs.changes_detected }}
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+              with:
+                  fetch-depth: 0  # We need full history to compare against the commit before the push
+
+            - name: Check for relevant file changes
+              id: check_changes
               run: |
-                  pre-commit run --all-files
+                  echo "=== Debug: Checking for changes in push ==="
+                  echo "Commit before push: ${{ github.event.before }}"
+                  echo "Latest commit in push: ${{ github.sha }}"
+
+                  echo "=== Debug: All changed files ==="
+                  git diff --name-only ${{ github.event.before }} ${{ github.sha }}
+
+                  if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -q -E '^(fitnessllm_dataplatform/|Dockerfile|pyproject.toml|poetry.lock|.github/)'; then
+                    echo "changes_detected=true" >> $GITHUB_OUTPUT
+                  else
+                    echo "changes_detected=false" >> $GITHUB_OUTPUT
+                  fi
+
+    cloud_run_job_deploy:
+        needs: cloud_run_job_detect_changes
+        if: needs.cloud_run_job_detect_changes.outputs.changes_detected == 'true' || (github.event_name == 'workflow_dispatch' && github.event.inputs.force_deploy
+            == 'true')
+        runs-on: ubuntu-latest
+        environment: dev
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+              with:
+                  fetch-depth: 3
 
             - name: Authenticate to Google Cloud
               uses: google-github-actions/auth@v2
@@ -188,6 +213,7 @@ jobs:
                     --set-env-vars "$ENV_VARS"
 
     cloud_function_detect_changes:
+        needs: testing
         runs-on: ubuntu-latest
         outputs:
             token_refresh: ${{ steps.check_changes.outputs.token_refresh }}
@@ -264,14 +290,7 @@ jobs:
             - uses: actions/checkout@v4
 
             - name: Copy shared code into each function directory
-              run: |
-                  for fn in cloud_functions/*; do
-                    if [ -d "$fn" ] && [ "$(basename $fn)" != "shared" ]; then
-                      rm -rf "$fn/shared"
-                      mkdir -p "$fn/shared"
-                      cp -r cloud_functions/shared/. "$fn/shared/"
-                    fi
-                  done
+              uses: ./.github/actions/copy-shared
 
             - name: Authenticate to Google Cloud
               if: needs.cloud_function_detect_changes.outputs[matrix.function] == 'true'
diff --git a/Makefile b/Makefile
index 5c6c7e34..aaa79a90 100644
--- a/Makefile
+++ b/Makefile
@@ -13,9 +13,11 @@ clean:
 	poetry install --sync
 
 build:
+	make copy_shared
 	docker build . -t fitnessllm-dp
 
 test:
+	make copy_shared
 	docker run -it \
 	  -e POETRY_VIRTUALENVS_CREATE=false \
 	  -e POETRY_NO_INTERACTION=1 \
@@ -28,6 +30,7 @@ coverage:
 	coverage
 
 run:
+	make copy_shared
 	docker run -it \
 	  --entrypoint /bin/bash \
 	  -v "$$PWD:/app/fitnessllm-dataplatform" \
diff --git a/README.md b/README.md
index 358d1a94..725e738d 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
 # fitnessllm-dataplatform
+
+[![codecov](https://codecov.io/gh/santoshgdev/fitnessllm-dataplatform/graph/badge.svg?token=7448321QM5)](https://codecov.io/gh/santoshgdev/fitnessllm-dataplatform)
diff --git a/cloud_functions/shared/__init__.py b/cloud_functions/shared/__init__.py
new file mode 100644
index 00000000..14e8999c
--- /dev/null
+++ b/cloud_functions/shared/__init__.py
@@ -0,0 +1 @@
+"""Init."""
diff --git a/cloud_functions/shared/cloud_utils.py b/cloud_functions/shared/cloud_utils.py
index 4d81b69b..f5ce7d49 100644
--- a/cloud_functions/shared/cloud_utils.py
+++ b/cloud_functions/shared/cloud_utils.py
@@ -1,32 +1,32 @@
-"""Cloud utils."""
-import json
-from os import environ
-
-from beartype import beartype
-from google.cloud import secretmanager
-
-from ..shared.logger_utils import create_structured_logger
-
-structured_logger = create_structured_logger(__name__)
-
-
-@beartype
-def get_secret(name: str) -> dict:
-    """Retrieve secret from secret manager."""
-    if "PROJECT_ID" not in environ:
-        raise KeyError("PROJECT_ID environment variable not found")
-    structured_logger(message="Initializing secret manager")
-    client = secretmanager.SecretManagerServiceClient()
-    structured_logger(message=f"Getting secret for {name}")
-    try:
-        response = client.access_secret_version(
-            request={
-                "name": f"projects/{environ['PROJECT_ID']}/secrets/{name}/versions/latest",
-            }
-        )
-        structured_logger(message=f"Retrieved secret {name}")
-        secret_payload = response.payload.data.decode("UTF-8")
-        return json.loads(secret_payload)
-    except Exception as e:
-        structured_logger(message=f"Failed to retrieve or decode secret {name}: {e}")
-        raise
+"""Cloud utils."""
+import json
+from os import environ
+
+from beartype import beartype
+from google.cloud import secretmanager
+
+from ..shared.logger_utils import create_structured_logger
+
+structured_logger = create_structured_logger(__name__)
+
+
+@beartype
+def get_secret(name: str) -> dict:
+    """Retrieve secret from secret manager."""
+    if "PROJECT_ID" not in environ:
+        raise KeyError("PROJECT_ID environment variable not found")
+    structured_logger(message="Initializing secret manager")
+    client = secretmanager.SecretManagerServiceClient()
+    structured_logger(message=f"Getting secret for {name}")
+    try:
+        response = client.access_secret_version(
+            request={
+                "name": f"projects/{environ['PROJECT_ID']}/secrets/{name}/versions/latest",
+            }
+        )
+        structured_logger(message=f"Retrieved secret {name}")
+        secret_payload = response.payload.data.decode("UTF-8")
+        return json.loads(secret_payload)
+    except Exception as e:
+        structured_logger(message=f"Failed to retrieve or decode secret {name}: {e}")
+        raise
diff --git a/cloud_functions/shared/logger_utils.py b/cloud_functions/shared/logger_utils.py
index f4ba81c7..3224e199 100644
--- a/cloud_functions/shared/logger_utils.py
+++ b/cloud_functions/shared/logger_utils.py
@@ -1,70 +1,70 @@
-"""Logging utilities."""
-import json
-import logging
-from functools import partial
-from logging import Logger
-from typing import Callable
-
-from beartype.typing import Optional
-
-
-def setup_logger(name: Optional[str] = None, level: int = logging.DEBUG) -> Logger:
-    """Sets up a logger with a console handler.
-
-    Args:
-        name (str): Name of the logger.
-        level (int): Logging level (e.g., logging.DEBUG, logging.INFO).
-
-    Returns:
-        logging.Logger: Configured logger instance.
-    """
-    # Create a custom logger
-    logger = logging.getLogger(name)
-    logger.setLevel(level)
-
-    # Prevent adding duplicate handlers if this function is called multiple times
-    if logger.hasHandlers():
-        return logger
-
-    console_handler = logging.StreamHandler()
-    console_handler.setLevel(level)
-
-    formatter = logging.Formatter(
-        "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
-    )
-    console_handler.setFormatter(formatter)
-
-    logger.addHandler(console_handler)
-
-    return logger
-
-
-logger = setup_logger()
-
-
-def log_structured(function_name: str, message: str, **kwargs: object) -> None:
-    """Helper function to log in structured JSON format.
-
-    Args:
-        function_name: Name of the function generating the log
-        message: The main log message
-        **kwargs: Additional key-value pairs to include in the log
-    """
-    log_data = {
-        "function": function_name,
-        "message": f"{function_name}-{message}",
-        **kwargs,
-    }
-    logger.info(json.dumps(log_data))
-
-
-def create_structured_logger(function_name: str) -> Callable:
-    """Create a structured logger for a specific function.
-
-    Args:
-        function_name (str): The name of the function to log.
-
-    Returns:
-        Callable: A partial function that logs in a structured format.
-    """
-    return partial(log_structured, function_name=function_name)
+"""Logging utilities."""
+import json
+import logging
+from functools import partial
+from logging import Logger
+from typing import Callable
+
+from beartype.typing import Optional
+
+
+def setup_logger(name: Optional[str] = None, level: int = logging.DEBUG) -> Logger:
+    """Sets up a logger with a console handler.
+
+    Args:
+        name (str): Name of the logger.
+        level (int): Logging level (e.g., logging.DEBUG, logging.INFO).
+
+    Returns:
+        logging.Logger: Configured logger instance.
+    """
+    # Create a custom logger
+    logger = logging.getLogger(name)
+    logger.setLevel(level)
+
+    # Prevent adding duplicate handlers if this function is called multiple times
+    if logger.hasHandlers():
+        return logger
+
+    console_handler = logging.StreamHandler()
+    console_handler.setLevel(level)
+
+    formatter = logging.Formatter(
+        "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+    )
+    console_handler.setFormatter(formatter)
+
+    logger.addHandler(console_handler)
+
+    return logger
+
+
+logger = setup_logger()
+
+
+def log_structured(function_name: str, message: str, **kwargs: object) -> None:
+    """Helper function to log in structured JSON format.
+
+    Args:
+        function_name: Name of the function generating the log
+        message: The main log message
+        **kwargs: Additional key-value pairs to include in the log
+    """
+    log_data = {
+        "function": function_name,
+        "message": f"{function_name}-{message}",
+        **kwargs,
+    }
+    logger.info(json.dumps(log_data))
+
+
+def create_structured_logger(function_name: str) -> Callable:
+    """Create a structured logger for a specific function.
+
+    Args:
+        function_name (str): The name of the function to log.
+
+    Returns:
+        Callable: A partial function that logs in a structured format.
+    """
+    return partial(log_structured, function_name=function_name)
diff --git a/cloud_functions/shared/task_utils.py b/cloud_functions/shared/task_utils.py
index 8bec7f6b..96fbd7a3 100644
--- a/cloud_functions/shared/task_utils.py
+++ b/cloud_functions/shared/task_utils.py
@@ -1,56 +1,53 @@
-"""Task utils."""
-import base64
-import os
-
-from beartype import beartype
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives import padding
-from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
-
-
-@beartype
-def encrypt_token(token: str, key: str) -> str:
-    """Encrypt a token using AES-256-CBC with PKCS#7 padding.
-
-    Args:
-        token (str): The plaintext token to be encrypted.
-        key (str): The encryption key. This key will be encoded as UTF-8 and adjusted to 32 bytes.
-
-    Returns:
-        str: The encrypted token in the format "iv:encrypted", where both the IV and the ciphertext are base64-encoded.
-    """
-    # Convert the plaintext token to bytes.
-    token_bytes = token.encode("utf-8")
-
-    # Convert the key to bytes if necessary.
-    if isinstance(key, str):
-        key_bytes = key.encode("utf-8")
-    else:
-        key_bytes = key
-
-    # Ensure the key is exactly 32 bytes long (AES-256 requirement).
-    if len(key_bytes) < 32:
-        key_bytes = key_bytes.ljust(32, b"\0")  # Pad with zeros.
-    elif len(key_bytes) > 32:
-        key_bytes = key_bytes[:32]  # Truncate if longer than 32 bytes.
-
-    # Generate a random 16-byte initialization vector (IV).
-    iv = os.urandom(16)
-
-    # Apply PKCS#7 padding to the plaintext token.
-    padder = padding.PKCS7(
-        128
-    ).padder()  # The block size is 128 bits (16 bytes) for AES.
-    padded_data = padder.update(token_bytes) + padder.finalize()
-
-    # Create the AES cipher in CBC mode.
-    cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=default_backend())
-    encryptor = cipher.encryptor()
-    encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
-
-    # Base64-encode the IV and encrypted data.
-    iv_encoded = base64.b64encode(iv).decode("utf-8")
-    encrypted_encoded = base64.b64encode(encrypted_data).decode("utf-8")
-
-    # Return the combined result in the format "iv:encrypted".
-    return f"{iv_encoded}:{encrypted_encoded}"
+"""Task utils."""
+import base64
+import os
+
+from beartype import beartype
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import padding
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+
+
+@beartype
+def encrypt_token(token: str, key: str) -> str:
+    """Encrypt a token using AES-256-CBC with PKCS#7 padding.
+
+    Args:
+        token (str): The plaintext token to be encrypted.
+        key (str): The encryption key. This key will be encoded as UTF-8 and adjusted to 32 bytes.
+
+    Returns:
+        str: The encrypted token in the format "iv:encrypted", where both the IV and the ciphertext are base64-encoded.
+    """
+    # Convert the plaintext token to bytes.
+    token_bytes = token.encode("utf-8")
+
+    # Convert the key to bytes if necessary.
+    # Convert the key to bytes if necessary.
+    key_bytes = key.encode("utf-8") if isinstance(key, str) else key
+    # Ensure the key is exactly 32 bytes long (AES-256 requirement).
+    if len(key_bytes) < 32:
+        key_bytes = key_bytes.ljust(32, b"\0")  # Pad with zeros.
+    elif len(key_bytes) > 32:
+        key_bytes = key_bytes[:32]  # Truncate if longer than 32 bytes.
+
+    # Generate a random 16-byte initialization vector (IV).
+    iv = os.urandom(16)
+
+    # Apply PKCS#7 padding to the plaintext token.
+    padder = padding.PKCS7(
+        128,
+    ).padder()  # The block size is 128 bits (16 bytes) for AES.
+    padded_data = padder.update(token_bytes) + padder.finalize()
+
+    # Create the AES cipher in CBC mode.
+    cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=default_backend())
+    encryptor = cipher.encryptor()
+    encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
+
+    # Base64-encode the IV and encrypted data.
+    iv_encoded = base64.b64encode(iv).decode("utf-8")
+    encrypted_encoded = base64.b64encode(encrypted_data).decode("utf-8")
+
+    # Return the combined result in the format "iv:encrypted".
+    return f"{iv_encoded}:{encrypted_encoded}"
diff --git a/cloud_functions/token_refresh/main.py b/cloud_functions/token_refresh/main.py
index 619e5b53..f9016605 100644
--- a/cloud_functions/token_refresh/main.py
+++ b/cloud_functions/token_refresh/main.py
@@ -1,314 +1,314 @@
-"""Main Entry point for Token Refresh."""
-import json
-import traceback
-
-from firebase_admin import auth, initialize_app
-from firebase_functions import https_fn, options
-from google.cloud import firestore
-
-from .shared.logger_utils import create_structured_logger
-from .streams.strava import strava_refresh_oauth_token
-
-structured_logger = create_structured_logger(__name__)
-
-try:
-    initialize_app()
-    structured_logger(message="Firebase Admin initialized successfully")
-except Exception as e:
-    structured_logger(
-        message="Error initializing Firebase Admin",
-        error=str(e),
-        level="ERROR",
-        traceback=traceback.format_exc(),
-    )
-    raise
-
-
-@https_fn.on_request(
-    cors=options.CorsOptions(cors_origins=["*"], cors_methods=["POST", "OPTIONS"])
-)
-def token_refresh(request: https_fn.Request) -> https_fn.Response:
-    """Cloud function taking http parameters to perform update of tokens.
-
-    Args:
-        request (http request): http request.
-
-    Note: At current time, it registers the parameters uid (firebase user id) and data_source.
-    """
-    # Log all request details at the start
-    structured_logger(
-        message="Request received",
-        method=request.method,
-        headers=dict(request.headers),
-        url=request.url,
-        args=dict(request.args),
-    )
-    try:
-        body = request.get_json(silent=True)
-        structured_logger(message="Request body", body=body)
-    except Exception as e:
-        structured_logger(
-            message="Error parsing request body",
-            error=str(e),
-            level="ERROR",
-            traceback=traceback.format_exc(),
-        )
-
-    # Handle OPTIONS request for CORS preflight
-    if request.method == "OPTIONS":
-        return https_fn.Response(
-            status=204,
-            headers={
-                "Access-Control-Allow-Origin": "*",
-                "Access-Control-Allow-Methods": "POST",
-                "Access-Control-Allow-Headers": "Authorization, Content-Type",
-                "Access-Control-Max-Age": "3600",
-            },
-        )
-
-    # Get data_source from query parameters instead of body
-    data_source = request.args.get("data_source")
-    if not data_source:
-        structured_logger(message="Missing data_source parameter", level="ERROR")
-        return https_fn.Response(
-            status=400,
-            response=json.dumps(
-                {
-                    "error": "Bad Request",
-                    "message": "Required data_source parameter is missing!",
-                }
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-
-    try:
-        # Log all headers for debugging
-        all_headers = dict(request.headers)
-        structured_logger(
-            message="Received headers",
-            headers=all_headers,
-            auth_header=request.headers.get(
-                "Authorization", "No Authorization header found"
-            ),
-        )
-
-        # Get the Authorization header
-        auth_header = request.headers.get("Authorization", "")
-        if not auth_header.startswith("Bearer "):
-            structured_logger(
-                message="Invalid Authorization header",
-                received_header=auth_header,
-                level="ERROR",
-            )
-            raise auth.InvalidIdTokenError("No valid authorization header found")
-
-        # Extract the token from the Authorization header
-        token = auth_header.split("Bearer ")[1]
-        if not token:
-            structured_logger(message="Empty Bearer token", level="ERROR")
-            raise auth.InvalidIdTokenError("Empty Bearer token")
-
-        # Verify the Firebase ID token and log its contents
-        decoded_token = auth.verify_id_token(token)
-        structured_logger(
-            message="Decoded token contents",
-            token_empty=decoded_token is None or decoded_token == "",
-        )
-
-        uid = decoded_token.get("uid") or decoded_token.get("sub")
-        if not uid:
-            raise auth.InvalidIdTokenError("No uid or sub claim found in token")
-
-        structured_logger(message="Token verified", uid=uid)
-
-        db = firestore.Client()
-        doc = db.collection("users").document(uid).get()
-
-        if not doc.exists:
-            structured_logger(message="User not found", uid=uid, level="ERROR")
-            return https_fn.Response(
-                status=404,
-                response=json.dumps(
-                    {
-                        "error": "Not Found",
-                        "message": f"User {uid} does not exist in Firestore",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        stream_doc = (
-            db.collection("users")
-            .document(uid)
-            .collection("stream")
-            .document(data_source)
-            .get()
-        )
-        if not stream_doc.exists:
-            # handle error (e.g., return 404 or similar)
-            return https_fn.Response(
-                status=404,
-                response=json.dumps(
-                    {
-                        "error": "Not Found",
-                        "message": f"Stream data for user {uid} and data source {data_source} not found",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        stream_data = stream_doc.to_dict()
-
-        if not stream_data or not stream_data.get("refreshToken"):
-            structured_logger(
-                message="No refresh token found",
-                uid=uid,
-                data_source=data_source,
-                level="ERROR",
-            )
-            return https_fn.Response(
-                status=400,
-                response=json.dumps(
-                    {
-                        "error": "Bad Request",
-                        "message": f"Bad Request - No refresh token found for user {uid} and data source {data_source}",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        if data_source == "strava":
-            try:
-                strava_refresh_oauth_token(db, uid, stream_data["refreshToken"])
-                structured_logger(
-                    message="Token refresh successful",
-                    uid=uid,
-                    data_source=data_source,
-                )
-                return https_fn.Response(
-                    status=200,
-                    response=json.dumps(
-                        {"message": "Token refreshed successfully for Strava."}
-                    ),
-                    headers={
-                        "Content-Type": "application/json",
-                        "Access-Control-Allow-Origin": "*",
-                    },
-                )
-            except ValueError as e:
-                if "credentials not found" in str(e):
-                    structured_logger(
-                        message="Strava credentials not found",
-                        error=str(e),
-                        level="ERROR",
-                        traceback=traceback.format_exc(),
-                    )
-                    return https_fn.Response(
-                        status=500,
-                        response=json.dumps(
-                            {
-                                "error": "Internal Server Error",
-                                "message": "Internal Server Error - Strava credentials not found in Secret Manager",
-                            }
-                        ),
-                        headers={
-                            "Content-Type": "application/json",
-                            "Access-Control-Allow-Origin": "*",
-                        },
-                    )
-                raise
-        else:
-            structured_logger(
-                message="Unsupported data source",
-                data_source=data_source,
-                level="ERROR",
-            )
-            return https_fn.Response(
-                status=400,
-                response=json.dumps(
-                    {
-                        "error": "Bad Request",
-                        "message": f"Bad Request - Unsupported data source: {data_source}",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-    except auth.InvalidIdTokenError:
-        structured_logger(
-            message="Invalid Firebase ID Token; JWT Token Issue",
-            level="ERROR",
-            auth=auth_header,
-            traceback=traceback.format_exc(),
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {
-                    "error": "Invalid Token",
-                    "message": "Invalid Firebase ID Token; JWT Token Issue",
-                }
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except auth.ExpiredIdTokenError:
-        structured_logger(
-            message="Expired token", level="ERROR", traceback=traceback.format_exc()
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {"error": "Unauthorized", "message": "Unauthorized - Expired token"}
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except auth.RevokedIdTokenError:
-        structured_logger(
-            message="Revoked token", level="ERROR", traceback=traceback.format_exc()
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {"error": "Unauthorized", "message": "Unauthorized - Revoked token"}
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except Exception as e:
-        structured_logger(
-            message="Error in token refresh",
-            error=str(e),
-            level="ERROR",
-            traceback=traceback.format_exc(),
-        )
-        return https_fn.Response(
-            status=500,
-            response=json.dumps({"error": "Internal Server Error", "message": str(e)}),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
+"""Main Entry point for Token Refresh."""
+import json
+import traceback
+
+from firebase_admin import auth, initialize_app
+from firebase_functions import https_fn, options
+from google.cloud import firestore
+
+from .shared.logger_utils import create_structured_logger
+from .streams.strava import strava_refresh_oauth_token
+
+structured_logger = create_structured_logger(__name__)
+
+try:
+    initialize_app()
+    structured_logger(message="Firebase Admin initialized successfully")
+except Exception as e:
+    structured_logger(
+        message="Error initializing Firebase Admin",
+        error=str(e),
+        level="ERROR",
+        traceback=traceback.format_exc(),
+    )
+    raise
+
+
+@https_fn.on_request(
+    cors=options.CorsOptions(cors_origins=["*"], cors_methods=["POST", "OPTIONS"])
+)
+def token_refresh(request: https_fn.Request) -> https_fn.Response:
+    """Cloud function taking http parameters to perform update of tokens.
+
+    Args:
+        request (http request): http request.
+
+    Note: At current time, it registers the parameters uid (firebase user id) and data_source.
+    """
+    # Log all request details at the start
+    structured_logger(
+        message="Request received",
+        method=request.method,
+        headers=dict(request.headers),
+        url=request.url,
+        args=dict(request.args),
+    )
+    try:
+        body = request.get_json(silent=True)
+        structured_logger(message="Request body", body=body)
+    except Exception as e:
+        structured_logger(
+            message="Error parsing request body",
+            error=str(e),
+            level="ERROR",
+            traceback=traceback.format_exc(),
+        )
+
+    # Handle OPTIONS request for CORS preflight
+    if request.method == "OPTIONS":
+        return https_fn.Response(
+            status=204,
+            headers={
+                "Access-Control-Allow-Origin": "*",
+                "Access-Control-Allow-Methods": "POST",
+                "Access-Control-Allow-Headers": "Authorization, Content-Type",
+                "Access-Control-Max-Age": "3600",
+            },
+        )
+
+    # Get data_source from query parameters instead of body
+    data_source = request.args.get("data_source")
+    if not data_source:
+        structured_logger(message="Missing data_source parameter", level="ERROR")
+        return https_fn.Response(
+            status=400,
+            response=json.dumps(
+                {
+                    "error": "Bad Request",
+                    "message": "Required data_source parameter is missing!",
+                }
+            ),
+            headers={
+                "Content-Type": "application/json",
+                "Access-Control-Allow-Origin": "*",
+            },
+        )
+
+    try:
+        # Log all headers for debugging
+        all_headers = dict(request.headers)
+        structured_logger(
+            message="Received headers",
+            headers=all_headers,
+            auth_header=request.headers.get(
+                "Authorization", "No Authorization header found"
+            ),
+        )
+
+        # Get the Authorization header
+        auth_header = request.headers.get("Authorization", "")
+        if not auth_header.startswith("Bearer "):
+            structured_logger(
+                message="Invalid Authorization header",
+                received_header=auth_header,
+                level="ERROR",
+            )
+            raise auth.InvalidIdTokenError("No valid authorization header found")
+
+        # Extract the token from the Authorization header
+        token = auth_header.split("Bearer ")[1]
+        if not token:
+            structured_logger(message="Empty Bearer token", level="ERROR")
+            raise auth.InvalidIdTokenError("Empty Bearer token")
+
+        # Verify the Firebase ID token and log its contents
+        decoded_token = auth.verify_id_token(token)
+        structured_logger(
+            message="Decoded token contents",
+            token_empty=decoded_token is None or decoded_token == "",
+        )
+
+        uid = decoded_token.get("uid") or decoded_token.get("sub")
+        if not uid:
+            raise auth.InvalidIdTokenError("No uid or sub claim found in token")
+
+        structured_logger(message="Token verified", uid=uid)
+
+        db = firestore.Client()
+        doc = db.collection("users").document(uid).get()
+
+        if not doc.exists:
+            structured_logger(message="User not found", uid=uid, level="ERROR")
+            return https_fn.Response(
+                status=404,
+                response=json.dumps(
+                    {
+                        "error": "Not Found",
+                        "message": f"User {uid} does not exist in Firestore",
+                    }
+                ),
+                headers={
+                    "Content-Type": "application/json",
+                    "Access-Control-Allow-Origin": "*",
+                },
+            )
+
+        stream_doc = (
+            db.collection("users")
+            .document(uid)
+            .collection("stream")
+            .document(data_source)
+            .get()
+        )
+        if not stream_doc.exists:
+            # handle error (e.g., return 404 or similar)
+            return https_fn.Response(
+                status=404,
+                response=json.dumps(
+                    {
+                        "error": "Not Found",
+                        "message": f"Stream data for user {uid} and data source {data_source} not found",
+                    }
+                ),
+                headers={
+                    "Content-Type": "application/json",
+                    "Access-Control-Allow-Origin": "*",
+                },
+            )
+
+        stream_data = stream_doc.to_dict()
+
+        if not stream_data or not stream_data.get("refreshToken"):
+            structured_logger(
+                message="No refresh token found",
+                uid=uid,
+                data_source=data_source,
+                level="ERROR",
+            )
+            return https_fn.Response(
+                status=400,
+                response=json.dumps(
+                    {
+                        "error": "Bad Request",
+                        "message": f"Bad Request - No refresh token found for user {uid} and data source {data_source}",
+                    }
+                ),
+                headers={
+                    "Content-Type": "application/json",
+                    "Access-Control-Allow-Origin": "*",
+                },
+            )
+
+        if data_source == "strava":
+            try:
+                strava_refresh_oauth_token(db, uid, stream_data["refreshToken"])
+                structured_logger(
+                    message="Token refresh successful",
+                    uid=uid,
+                    data_source=data_source,
+                )
+                return https_fn.Response(
+                    status=200,
+                    response=json.dumps(
+                        {"message": "Token refreshed successfully for Strava."}
+                    ),
+                    headers={
+                        "Content-Type": "application/json",
+                        "Access-Control-Allow-Origin": "*",
+                    },
+                )
+            except ValueError as e:
+                if "credentials not found" in str(e):
+                    structured_logger(
+                        message="Strava credentials not found",
+                        error=str(e),
+                        level="ERROR",
+                        traceback=traceback.format_exc(),
+                    )
+                    return https_fn.Response(
+                        status=500,
+                        response=json.dumps(
+                            {
+                                "error": "Internal Server Error",
+                                "message": "Internal Server Error - Strava credentials not found in Secret Manager",
+                            }
+                        ),
+                        headers={
+                            "Content-Type": "application/json",
+                            "Access-Control-Allow-Origin": "*",
+                        },
+                    )
+                raise
+        else:
+            structured_logger(
+                message="Unsupported data source",
+                data_source=data_source,
+                level="ERROR",
+            )
+            return https_fn.Response(
+                status=400,
+                response=json.dumps(
+                    {
+                        "error": "Bad Request",
+                        "message": f"Bad Request - Unsupported data source: {data_source}",
+                    }
+                ),
+                headers={
+                    "Content-Type": "application/json",
+                    "Access-Control-Allow-Origin": "*",
+                },
+            )
+
+    except auth.InvalidIdTokenError:
+        structured_logger(
+            message="Invalid Firebase ID Token; JWT Token Issue",
+            level="ERROR",
+            auth=auth_header,
+            traceback=traceback.format_exc(),
+        )
+        return https_fn.Response(
+            status=401,
+            response=json.dumps(
+                {
+                    "error": "Invalid Token",
+                    "message": "Invalid Firebase ID Token; JWT Token Issue",
+                }
+            ),
+            headers={
+                "Content-Type": "application/json",
+                "Access-Control-Allow-Origin": "*",
+            },
+        )
+    except auth.ExpiredIdTokenError:
+        structured_logger(
+            message="Expired token", level="ERROR", traceback=traceback.format_exc()
+        )
+        return https_fn.Response(
+            status=401,
+            response=json.dumps(
+                {"error": "Unauthorized", "message": "Unauthorized - Expired token"}
+            ),
+            headers={
+                "Content-Type": "application/json",
+                "Access-Control-Allow-Origin": "*",
+            },
+        )
+    except auth.RevokedIdTokenError:
+        structured_logger(
+            message="Revoked token", level="ERROR", traceback=traceback.format_exc()
+        )
+        return https_fn.Response(
+            status=401,
+            response=json.dumps(
+                {"error": "Unauthorized", "message": "Unauthorized - Revoked token"}
+            ),
+            headers={
+                "Content-Type": "application/json",
+                "Access-Control-Allow-Origin": "*",
+            },
+        )
+    except Exception as e:
+        structured_logger(
+            message="Error in token refresh",
+            error=str(e),
+            level="ERROR",
+            traceback=traceback.format_exc(),
+        )
+        return https_fn.Response(
+            status=500,
+            response=json.dumps({"error": "Internal Server Error", "message": str(e)}),
+            headers={
+                "Content-Type": "application/json",
+                "Access-Control-Allow-Origin": "*",
+            },
+        )
diff --git a/cloud_functions/token_refresh/streams/strava.py b/cloud_functions/token_refresh/streams/strava.py
index 3bd47317..984c4a02 100644
--- a/cloud_functions/token_refresh/streams/strava.py
+++ b/cloud_functions/token_refresh/streams/strava.py
@@ -1,113 +1,118 @@
-"""Strava specific utils."""
-import os
-from os import environ
-
-from beartype import beartype
-from beartype.typing import Any, Dict
-from google.cloud import firestore
-from stravalib.client import Client
-
-from ..shared.cloud_utils import get_secret
-from ..shared.logger_utils import create_structured_logger
-from ..shared.task_utils import encrypt_token
-from ..utils.task_utils import decrypt_token, update_last_refresh
-
-structured_logger = create_structured_logger(__name__)
-
-
-@beartype
-def strava_refresh_oauth_token(
-    db: firestore.Client, uid: str, refresh_token: str
-) -> None:
-    """Call Strava OAuth to refresh the token.
-
-    Args:
-        db: Firestore client.
-        uid: Firestore user id.
-        refresh_token: Encrypted Strava OAuth refresh token.
-
-    Raises:
-        ValueError: If refresh token is invalid.
-    """
-    structured_logger(message="Starting token refresh", uid=uid)
-
-    encryption_key = get_secret(os.environ["ENCRYPTION_SECRET"])["token"]
-
-    client = Client()
-    strava_secret = get_secret(environ["STRAVA_SECRET"])
-    client_id = strava_secret.get("client_id")
-    client_secret = strava_secret.get("client_secret")
-
-    if not client_id or not client_secret:
-        structured_logger(message="Strava credentials not found", level="ERROR")
-        raise ValueError("Strava credentials not found in Secret Manager")
-
-    try:
-        token_response = client.refresh_access_token(
-            client_id=client_id,
-            client_secret=client_secret,
-            refresh_token=decrypt_token(refresh_token, encryption_key),
-        )
-        structured_logger(message="Token refresh successful", uid=uid)
-
-        new_tokens = {
-            "accessToken": encrypt_token(
-                token_response["access_token"], encryption_key
-            ),
-            "refreshToken": encrypt_token(
-                token_response["refresh_token"], encryption_key
-            ),
-            "expiresAt": token_response["expires_at"],
-            "lastTokenRefresh": update_last_refresh(),
-        }
-
-        strava_update_user_tokens(db=db, uid=uid, new_tokens=new_tokens)
-        structured_logger(message="Tokens updated in Firestore", uid=uid)
-    except Exception as e:
-        structured_logger(
-            message="Error refreshing token",
-            uid=uid,
-            error=str(e),
-            level="ERROR",
-        )
-        raise
-
-
-# Bear type is removed here due to a test that has a testing component located in tests/.
-# This is done so that the testing components don't need to be shipped with the production code.
-def strava_update_user_tokens(
-    db: firestore.Client, uid: str, new_tokens: Dict[str, Any]
-) -> None:
-    """Update user document with new tokens.
-
-    Args:
-        db: Firestore client.
-        uid: Firestore user id.
-        new_tokens: New tokens.
-    """
-    structured_logger(message="Updating user tokens", uid=uid)
-
-    strava_ref = (
-        db.collection("users").document(uid).collection("stream").document("strava")
-    )
-
-    doc = strava_ref.get()
-    if not doc.exists:
-        structured_logger(
-            message="Strava document doesn't exist in stream subcollection",
-            uid=uid,
-            level="ERROR",
-        )
-        # Create the document with default values
-        strava_ref.set(new_tokens, merge=True)
-        return
-
-    strava_ref.update(
-        {
-            "accessToken": new_tokens["accessToken"],
-            "refreshToken": new_tokens["refreshToken"],
-            "expiresAt": new_tokens["expiresAt"],
-            "lastTokenRefresh": new_tokens["lastTokenRefresh"],
-        }
-    )
-    structured_logger(message="User tokens updated successfully", uid=uid)
+"""Strava specific utils."""
+from os import environ
+
+from beartype import beartype
+from beartype.typing import Any, Dict
+from google.cloud import firestore
+from stravalib.client import Client
+
+from ..shared.cloud_utils import get_secret
+from ..shared.logger_utils import create_structured_logger
+from ..shared.task_utils import encrypt_token
+from ..utils.task_utils import decrypt_token, update_last_refresh
+
+structured_logger = create_structured_logger(__name__)
+
+
+@beartype
+def strava_refresh_oauth_token(
+    db: firestore.Client,
+    uid: str,
+    refresh_token: str,
+) -> None:
+    """Call Strava OAuth to refresh the token.
+
+    Args:
+        db: Firestore client.
+        uid: Firestore user id.
+        refresh_token: Encrypted Strava OAuth refresh token.
+
+    Raises:
+        ValueError: If refresh token is invalid.
+    """
+    structured_logger(message="Starting token refresh", uid=uid)
+
+    encryption_key = get_secret(environ["ENCRYPTION_SECRET"])["token"]
+
+    client = Client()
+    strava_secret = get_secret(environ["STRAVA_SECRET"])
+    client_id = strava_secret.get("client_id")
+    client_secret = strava_secret.get("client_secret")
+
+    if not client_id or not client_secret:
+        structured_logger(message="Strava credentials not found", level="ERROR")
+        raise ValueError("Strava credentials not found in Secret Manager")
+
+    try:
+        token_response = client.refresh_access_token(
+            client_id=client_id,
+            client_secret=client_secret,
+            refresh_token=decrypt_token(refresh_token, encryption_key),
+        )
+        structured_logger(message="Token refresh successful", uid=uid)
+
+        new_tokens = {
+            "accessToken": encrypt_token(
+                token_response["access_token"],
+                encryption_key,
+            ),
+            "refreshToken": encrypt_token(
+                token_response["refresh_token"],
+                encryption_key,
+            ),
+            "expiresAt": token_response["expires_at"],
+            "lastTokenRefresh": update_last_refresh(),
+        }
+
+        strava_update_user_tokens(db=db, uid=uid, new_tokens=new_tokens)
+        structured_logger(message="Tokens updated in Firestore", uid=uid)
+    except Exception as e:
+        structured_logger(
+            message="Error refreshing token",
+            uid=uid,
+            error=str(e),
+            level="ERROR",
+        )
+        raise
+
+
+# Bear type is removed here due to a test that has a testing component located in tests/.
+# This is done so that the testing components don't need to be shipped with the production code.
+def strava_update_user_tokens(
+    db: firestore.Client,
+    uid: str,
+    new_tokens: Dict[str, Any],
+) -> None:
+    """Update user document with new tokens.
+
+    Args:
+        db: Firestore client.
+        uid: Firestore user id.
+        new_tokens: New tokens.
+    """
+    structured_logger(message="Updating user tokens", uid=uid)
+
+    strava_ref = (
+        db.collection("users").document(uid).collection("stream").document("strava")
+    )
+
+    doc = strava_ref.get()
+    if not doc.exists:
+        structured_logger(
+            message="Strava document doesn't exist in stream subcollection",
+            uid=uid,
+            level="ERROR",
+        )
+        # Create the document with default values
+        strava_ref.set(new_tokens, merge=True)
+        return
+
+    strava_ref.update(
+        {
+            "accessToken": new_tokens["accessToken"],
+            "refreshToken": new_tokens["refreshToken"],
+            "expiresAt": new_tokens["expiresAt"],
+            "lastTokenRefresh": new_tokens["lastTokenRefresh"],
+        },
+    )
+    structured_logger(message="User tokens updated successfully", uid=uid)
diff --git a/poetry.lock b/poetry.lock
index dd13f0cd..7cf00a56 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -32,25 +32,6 @@ types-python-dateutil = ">=2.8.10"
 doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"]
 test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"]
 
-[[package]]
-name = "beartype"
-version = "0.19.0"
-description = "Unbearably fast near-real-time hybrid runtime-static type-checking in pure Python."
-optional = false
-python-versions = ">=3.8"
-groups = ["main"]
-files = [
-    {file = "beartype-0.19.0-py3-none-any.whl", hash = "sha256:33b2694eda0daf052eb2aff623ed9a8a586703bbf0a90bbc475a83bbf427f699"},
-    {file = "beartype-0.19.0.tar.gz", hash = "sha256:de42dfc1ba5c3710fde6c3002e3bd2cad236ed4d2aabe876345ab0b4234a6573"},
-]
-
-[package.extras]
-dev = ["autoapi (>=0.9.0)", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\"", "jax[cpu] ; sys_platform == \"linux\"", "jaxtyping ; sys_platform == \"linux\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "numba", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pydata-sphinx-theme (<=0.7.2)", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "sphinx (>=4.2.0,<6.0.0)", "sphinx ; python_version >= \"3.8.0\"", "sphinxext-opengraph (>=0.7.5)", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)"]
-doc-rtd = ["autoapi (>=0.9.0)", "pydata-sphinx-theme (<=0.7.2)", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)"]
-test = ["coverage (>=5.5)", "equinox ; sys_platform == \"linux\"", "jax[cpu] ; sys_platform == \"linux\"", "jaxtyping ; sys_platform == \"linux\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "numba", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "sphinx ; python_version >= \"3.8.0\"", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)"]
-test-tox = ["equinox ; sys_platform == \"linux\"", "jax[cpu] ; sys_platform == \"linux\"", "jaxtyping ; sys_platform == \"linux\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "numba", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "sphinx ; python_version >= \"3.8.0\"", "typing-extensions (>=3.10.0.0)"]
-test-tox-coverage = ["coverage (>=5.5)"]
-
 [[package]]
 name = "blinker"
 version = "1.9.0"
@@ -370,76 +351,6 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
-[[package]]
-name = "coverage"
-version = "7.6.11"
-description = "Code coverage measurement for Python"
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
-    {file = "coverage-7.6.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eafea49da254a8289bed3fab960f808b322eda5577cb17a3733014928bbfbebd"},
-    {file = "coverage-7.6.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a3f7cbbcb4ad95067a6525f83a6fc78d9cbc1e70f8abaeeaeaa72ef34f48fc3"},
-    {file = "coverage-7.6.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de6b079b39246a7da9a40cfa62d5766bd52b4b7a88cf5a82ec4c45bf6e152306"},
-    {file = "coverage-7.6.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60d4ad09dfc8c36c4910685faafcb8044c84e4dae302e86c585b3e2e7778726c"},
-    {file = "coverage-7.6.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e433b6e3a834a43dae2889adc125f3fa4c66668df420d8e49bc4ee817dd7a70"},
-    {file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ac5d92e2cc121a13270697e4cb37e1eb4511ac01d23fe1b6c097facc3b46489e"},
-    {file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5128f3ba694c0a1bde55fc480090392c336236c3e1a10dad40dc1ab17c7675ff"},
-    {file = "coverage-7.6.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:397489c611b76302dfa1d9ea079e138dddc4af80fc6819d5f5119ec8ca6c0e47"},
-    {file = "coverage-7.6.11-cp310-cp310-win32.whl", hash = "sha256:c7719a5e1dc93883a6b319bc0374ecd46fb6091ed659f3fbe281ab991634b9b0"},
-    {file = "coverage-7.6.11-cp310-cp310-win_amd64.whl", hash = "sha256:c27df03730059118b8a923cfc8b84b7e9976742560af528242f201880879c1da"},
-    {file = "coverage-7.6.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:532fe139691af134aa8b54ed60dd3c806aa81312d93693bd2883c7b61592c840"},
-    {file = "coverage-7.6.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0b0f272901a5172090c0802053fbc503cdc3fa2612720d2669a98a7384a7bec"},
-    {file = "coverage-7.6.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4bda710139ea646890d1c000feb533caff86904a0e0638f85e967c28cb8eec50"},
-    {file = "coverage-7.6.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a165b09e7d5f685bf659063334a9a7b1a2d57b531753d3e04bd442b3cfe5845b"},
-    {file = "coverage-7.6.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff136607689c1c87f43d24203b6d2055b42030f352d5176f9c8b204d4235ef27"},
-    {file = "coverage-7.6.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:050172741de03525290e67f0161ae5f7f387c88fca50d47fceb4724ceaa591d2"},
-    {file = "coverage-7.6.11-cp311-cp311-win32.whl", hash = "sha256:27700d859be68e4fb2e7bf774cf49933dcac6f81a9bc4c13bd41735b8d26a53b"},
-    {file = "coverage-7.6.11-cp311-cp311-win_amd64.whl", hash = "sha256:cd4839813b09ab1dd1be1bbc74f9a7787615f931f83952b6a9af1b2d3f708bf7"},
-    {file = "coverage-7.6.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbb1a822fd858d9853333a7c95d4e70dde9a79e65893138ce32c2ec6457d7a36"},
-    {file = "coverage-7.6.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61c834cbb80946d6ebfddd9b393a4c46bec92fcc0fa069321fcb8049117f76ea"},
-    {file = "coverage-7.6.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a46d56e99a31d858d6912d31ffa4ede6a325c86af13139539beefca10a1234ce"},
-    {file = "coverage-7.6.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b48db06f53d1864fea6dbd855e6d51d41c0f06c212c3004511c0bdc6847b297"},
-    {file = "coverage-7.6.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6ff5be3b1853e0862da9d349fe87f869f68e63a25f7c37ce1130b321140f963"},
-    {file = "coverage-7.6.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be05bde21d5e6eefbc3a6de6b9bee2b47894b8945342e8663192809c4d1f08ce"},
-    {file = "coverage-7.6.11-cp312-cp312-win32.whl", hash = "sha256:e3b746fa0ffc5b6b8856529de487da8b9aeb4fb394bb58de6502ef45f3434f12"},
-    {file = "coverage-7.6.11-cp312-cp312-win_amd64.whl", hash = "sha256:ac476e6d0128fb7919b3fae726de72b28b5c9644cb4b579e4a523d693187c551"},
-    {file = "coverage-7.6.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c86f4c7a6d1a54a24d804d9684d96e36a62d3ef7c0d7745ae2ea39e3e0293251"},
-    {file = "coverage-7.6.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7eb0504bb307401fd08bc5163a351df301438b3beb88a4fa044681295bbefc67"},
-    {file = "coverage-7.6.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca95d40900cf614e07f00cee8c2fad0371df03ca4d7a80161d84be2ec132b7a4"},
-    {file = "coverage-7.6.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db4b1a69976b1b02acda15937538a1d3fe10b185f9d99920b17a740a0a102e06"},
-    {file = "coverage-7.6.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf96beb05d004e4c51cd846fcdf9eee9eb2681518524b66b2e7610507944c2f"},
-    {file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08e5fb93576a6b054d3d326242af5ef93daaac9bb52bc25f12ccbc3fa94227cd"},
-    {file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25575cd5a7d2acc46b42711e8aff826027c0e4f80fb38028a74f31ac22aae69d"},
-    {file = "coverage-7.6.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8fa4fffd90ee92f62ff7404b4801b59e8ea8502e19c9bf2d3241ce745b52926c"},
-    {file = "coverage-7.6.11-cp313-cp313-win32.whl", hash = "sha256:0d03c9452d9d1ccfe5d3a5df0427705022a49b356ac212d529762eaea5ef97b4"},
-    {file = "coverage-7.6.11-cp313-cp313-win_amd64.whl", hash = "sha256:fd2fffc8ce8692ce540103dff26279d2af22d424516ddebe2d7e4d6dbb3816b2"},
-    {file = "coverage-7.6.11-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e7ac966ab110bd94ee844f2643f196d78fde1cd2450399116d3efdd706e19f5"},
-    {file = "coverage-7.6.11-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ba27a0375c5ef4d2a7712f829265102decd5ff78b96d342ac2fa555742c4f4f"},
-    {file = "coverage-7.6.11-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2778be4f574b39ec9dcd9e5e13644f770351ee0990a0ecd27e364aba95af89b"},
-    {file = "coverage-7.6.11-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5edc16712187139ab635a2e644cc41fc239bc6d245b16124045743130455c652"},
-    {file = "coverage-7.6.11-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6ff122a0a10a30121d9f0cb3fbd03a6fe05861e4ec47adb9f25e9245aabc19"},
-    {file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff562952f15eff27247a4c4b03e45ce8a82e3fb197de6a7c54080f9d4ba07845"},
-    {file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4f21e3617f48d683f30cf2a6c8b739c838e600cb1454fe6b2eb486ac2bce8fbd"},
-    {file = "coverage-7.6.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6d60577673ba48d8ae8e362e61fd4ad1a640293ffe8991d11c86f195479100b7"},
-    {file = "coverage-7.6.11-cp313-cp313t-win32.whl", hash = "sha256:13100f98497086b359bf56fc035a762c674de8ef526daa389ac8932cb9bff1e0"},
-    {file = "coverage-7.6.11-cp313-cp313t-win_amd64.whl", hash = "sha256:2c81e53782043b323bd34c7de711ed9b4673414eb517eaf35af92185b873839c"},
-    {file = "coverage-7.6.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ff52b4e2ac0080c96e506819586c4b16cdbf46724bda90d308a7330a73cc8521"},
-    {file = "coverage-7.6.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f4679fcc9eb9004fdd1b00231ef1ec7167168071bebc4d66327e28c1979b4449"},
-    {file = "coverage-7.6.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90de4e9ca4489e823138bd13098af9ac8028cc029f33f60098b5c08c675c7bda"},
-    {file = "coverage-7.6.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c96a142057d83ee993eaf71629ca3fb952cda8afa9a70af4132950c2bd3deb9"},
-    {file = "coverage-7.6.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:476f29a258b9cd153f2be5bf5f119d670d2806363595263917bddc167d6e5cce"},
-    {file = "coverage-7.6.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:09d03f48d9025b8a6a116cddcb6c7b8ce80e4fb4c31dd2e124a7c377036ad58e"},
-    {file = "coverage-7.6.11-cp39-cp39-win32.whl", hash = "sha256:bb35ae9f134fbd9cf7302a9654d5a1e597c974202678082dcc569eb39a8cde03"},
-    {file = "coverage-7.6.11-cp39-cp39-win_amd64.whl", hash = "sha256:f382004fa4c93c01016d9226b9d696a08c53f6818b7ad59b4e96cb67e863353a"},
-    {file = "coverage-7.6.11-pp39.pp310-none-any.whl", hash = "sha256:adc2d941c0381edfcf3897f94b9f41b1e504902fab78a04b1677f2f72afead4b"},
-    {file = "coverage-7.6.11-py3-none-any.whl", hash = "sha256:f0f334ae844675420164175bf32b04e18a81fe57ad8eb7e0cfd4689d681ffed7"},
-    {file = "coverage-7.6.11.tar.gz", hash = "sha256:e642e6a46a04e992ebfdabed79e46f478ec60e2c528e1e1a074d63800eda4286"},
-]
-
-[package.extras]
-toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
-
 [[package]]
 name = "cryptography"
 version = "44.0.2"
@@ -1999,25 +1910,6 @@ pluggy = ">=1.5,<2"
 [package.extras]
 dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
-[[package]]
-name = "pytest-cov"
-version = "6.0.0"
-description = "Pytest plugin for measuring coverage."
-optional = false
-python-versions = ">=3.9"
-groups = ["main"]
-files = [
-    {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"},
-    {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"},
-]
-
-[package.dependencies]
-coverage = {version = ">=7.5", extras = ["toml"]}
-pytest = ">=4.6"
-
-[package.extras]
-testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
-
 [[package]]
 name = "python-dateutil"
 version = "2.9.0.post0"
@@ -2417,4 +2309,4 @@ watchdog = ["watchdog (>=2.3)"]
 [metadata]
 lock-version = "2.1"
 python-versions = "3.12.2"
-content-hash = "3342ca699afbb9e027560edd36117746a7de2fa7a5be693257edc21df1ce72e5"
+content-hash = "6cd88b1671026c7f5732f30cec62b8aeee73534e2d98da9af4db16b0866a161d"
diff --git a/pyproject.toml b/pyproject.toml
index 2156aa1c..de4fc014 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,41 +1,40 @@
-[project]
-name = "fitnessllm_dataplatform"
-version = "0.1.0"
-description = ""
-authors = [
-    { name = "Santosh G", email = "santosh@santoshg.io" }
-]
-
-[tool.poetry.dependencies]
-python = "3.12.2"
-cloudpathlib = {extras = ["gs"], version = "^0.20.0"}
-typing-extensions = "^4.12.2"
-fire = "^0.7.0"
-google-cloud-secret-manager = "^2.22.1"
-tqdm = "^4.67.1"
-numpy = "^2.2.2"
-pandas = "^2.2.3"
-google-cloud-bigquery = {extras = ["pandas"], version = "^3.29.0"}
-joblib = "^1.4.2"
-tqdm-joblib = "^0.0.4"
-google-cloud-bigquery-storage = "^2.27.0"
-stravalib = "^2.1"
-pytest = "^8.3.4"
-pre-commit = "^4.1.0"
-firebase-admin = "^6.7.0"
-redis = "^5.2.1"
-jinja2 = "^3.1.5"
-sqlglot = "^26.7.0"
-
-[tool.poetry.dev-dependencies]
-beartype = "^0.19.0"
-pytest-cov = "^6.0.0"
-firebase-functions = "^0.4.2"
-faker = "^37.1.0"
-
-[build-system]
-requires = ["poetry-core>=1.0.0"]
-build-backend = "poetry.core.masonry.api"
-
-[tool.poetry.requires-plugins]
-poetry-plugin-export = ">=1.8"
+[project]
+name = "fitnessllm_dataplatform"
+version = "0.1.0"
+description = ""
+authors = [
+    { name = "Santosh G", email = "santosh@santoshg.io" }
+]
+
+[tool.poetry.dependencies]
+python = "^3.12.2"
+cloudpathlib = {extras = ["gs"], version = "^0.20.0"}
+typing-extensions = "^4.12.2"
+fire = "^0.7.0"
+google-cloud-secret-manager = "^2.22.1"
+tqdm = "^4.67.1"
+numpy = "^1.26.0"
+pandas = "^2.2.3"
+google-cloud-bigquery = {extras = ["pandas"], version = "^3.29.0"}
+joblib = "^1.4.2"
+tqdm-joblib = "^0.0.4"
+google-cloud-bigquery-storage = "^2.27.0"
+stravalib = "^2.1"
+pytest = "^8.3.4"
+pre-commit = "^4.1.0"
+firebase-admin = "^6.7.0"
+redis = "^5.2.1"
+jinja2 = "^3.1.5"
+sqlglot = "^26.7.0"
+faker = "^37.1.0"
+flask = "^3.1.0"
+firebase-functions = "^0.4.2"
+beartype = "^0.19.0"
+pytest-cov = "^6.0.0"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry.requires-plugins]
+poetry-plugin-export = ">=1.8"
diff --git a/tests/cloud_functions/testing_utils.py b/tests/cloud_functions/testing_utils.py
index 43346981..637c449e 100644
--- a/tests/cloud_functions/testing_utils.py
+++ b/tests/cloud_functions/testing_utils.py
@@ -1,7 +1,7 @@
 """Utilities for testing Firestore interactions in memory."""
 import uuid
 from collections import defaultdict
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from typing import Optional
 
 
@@ -183,7 +183,7 @@ def populate_inmemory_firestore_with_users_and_streams(
         user_id = str(uuid.uuid4()).replace("-", "")[:28]
         user_ids.append(user_id)
 
-        now = datetime.now().replace(tzinfo=datetime.timezone.utc)
+        now = datetime.now(timezone.utc)
 
         user_data = {
             "created_time": now,
diff --git a/tests/cloud_functions/token_refresh/streams/__init__.py b/tests/cloud_functions/token_refresh/streams/__init__.py
new file mode 100644
index 00000000..14e8999c
--- /dev/null
+++ b/tests/cloud_functions/token_refresh/streams/__init__.py
@@ -0,0 +1 @@
+"""Init."""
diff --git a/tests/cloud_functions/token_refresh/streams/test_strava.py b/tests/cloud_functions/token_refresh/streams/test_strava.py
index 12901004..6e12af34 100644
--- a/tests/cloud_functions/token_refresh/streams/test_strava.py
+++ b/tests/cloud_functions/token_refresh/streams/test_strava.py
@@ -1,36 +1,36 @@
-from cloud_functions.token_refresh.streams.strava import strava_update_user_tokens
-from tests.cloud_functions.testing_utils import InMemoryFirestoreClient
-
-
-def test_strava_update_user_tokens() -> None:
-    db = InMemoryFirestoreClient()
-    uid = "testuser123"
-
-    db.collection("users").document(uid).collection("stream").document("strava").set(
-        {
-            "accessToken": "old_access_token",
-            "refreshToken": "old_refresh_token",
-            "expiresAt": 123456,
-            "lastTokenRefresh": "old_time",
-        }
-    )
-    new_tokens = {
-        "accessToken": "new_access_token",
-        "refreshToken": "new_refresh_token",
-        "expiresAt": 654321,
-        "lastTokenRefresh": "new_time",
-    }
-    strava_update_user_tokens(db, uid, new_tokens)
-
-    updated_doc = (
-        db.collection("users")
-        .document(uid)
-        .collection("stream")
-        .document("strava")
-        .get()
-    )
-    updated_data = updated_doc.to_dict()
-    assert updated_data["accessToken"] == "new_access_token"
-    assert updated_data["refreshToken"] == "new_refresh_token"
-    assert updated_data["expiresAt"] == 654321
-    assert updated_data["lastTokenRefresh"] == "new_time"
+from cloud_functions.token_refresh.streams.strava import strava_update_user_tokens
+from tests.cloud_functions.testing_utils import InMemoryFirestoreClient
+
+
+def test_strava_update_user_tokens() -> None:
+    db = InMemoryFirestoreClient()
+    uid = "testuser123"
+
+    db.collection("users").document(uid).collection("stream").document("strava").set(
+        {
+            "accessToken": "old_access_token",
+            "refreshToken": "old_refresh_token",
+            "expiresAt": 123456,
+            "lastTokenRefresh": "old_time",
+        }
+    )
+    new_tokens = {
+        "accessToken": "new_access_token",
+        "refreshToken": "new_refresh_token",
+        "expiresAt": 654321,
+        "lastTokenRefresh": "new_time",
+    }
+    strava_update_user_tokens(db, uid, new_tokens)
+
+    updated_doc = (
+        db.collection("users")
+        .document(uid)
+        .collection("stream")
+        .document("strava")
+        .get()
+    )
+    updated_data = updated_doc.to_dict()
+    assert updated_data["accessToken"] == "new_access_token"
+    assert updated_data["refreshToken"] == "new_refresh_token"
+    assert updated_data["expiresAt"] == 654321
+    assert updated_data["lastTokenRefresh"] == "new_time"
diff --git a/tests/cloud_functions/token_refresh/test_main.py b/tests/cloud_functions/token_refresh/test_main.py
index 064681bb..aca4e013 100644
--- a/tests/cloud_functions/token_refresh/test_main.py
+++ b/tests/cloud_functions/token_refresh/test_main.py
@@ -1,117 +1,119 @@
-import json
-from unittest.mock import patch
-
-import flask
-from firebase_admin import auth
-
-from cloud_functions.token_refresh.main import token_refresh
-from tests.cloud_functions.testing_utils import (
-    populate_inmemory_firestore_with_users_and_streams,
-)
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                response = token_refresh(mock_request)
-                assert response.status_code == 200
-                assert (
-                    "Token refreshed successfully for Strava"
-                    in json.loads(response.response[0])["message"]
-                )
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_missing_data_source(
-    mock_verify, mock_request, mock_decoded_token
-):
-    """Test token refresh with missing data source."""
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                mock_request.args = {}
-                response = token_refresh(mock_request)
-                assert response.status_code == 400
-                assert (
-                    "Required data_source parameter is missing"
-                    in json.loads(response.response[0])["message"]
-                )
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_invalid_token(mock_verify, mock_request, mock_decoded_token):
-    """Test token refresh with invalid token."""
-
-    # Setup in-memory Firestore with one user and strava stream
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                mock_verify.side_effect = auth.InvalidIdTokenError("Invalid token")
-                response = token_refresh(mock_request)
-                assert response.status_code == 401
-                assert (
-                    "Invalid Firebase ID Token; JWT Token Issu"
-                    in json.loads(response.response[0])["message"]
-                )
+import json
+from unittest.mock import patch
+
+import flask
+from firebase_admin import auth
+
+from cloud_functions.token_refresh.main import token_refresh
+from tests.cloud_functions.testing_utils import (
+    populate_inmemory_firestore_with_users_and_streams,
+)
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                response = token_refresh(mock_request)
+                assert response.status_code == 200
+                assert (
+                    "Token refreshed successfully for Strava"
+                    in json.loads(response.response[0])["message"]
+                )
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_missing_data_source(
+    mock_verify,
+    mock_request,
+    mock_decoded_token,
+):
+    """Test token refresh with missing data source."""
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                mock_request.args = {}
+                response = token_refresh(mock_request)
+                assert response.status_code == 400
+                assert (
+                    "Required data_source parameter is missing"
+                    in json.loads(response.response[0])["message"]
+                )
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_invalid_token(mock_verify, mock_request, mock_decoded_token):
+    """Test token refresh with invalid token."""
+
+    # Setup in-memory Firestore with one user and strava stream
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                mock_verify.side_effect = auth.InvalidIdTokenError("Invalid token")
+                response = token_refresh(mock_request)
+                assert response.status_code == 401
+                assert (
+                    "Invalid Firebase ID Token; JWT Token Issue"
+                    in json.loads(response.response[0])["message"]
+                )


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 16**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>cloud_functions/token_refresh/main.py (1)</summary><blockquote>

`30-315`: _🧹 Nitpick (assertive)_

**Refactor `token_refresh` into smaller helper functions**

With many nested branches (authentication, CORS, Firestore lookup, per-source refresh, and multiple error handlers), this function exceeds recommended complexity thresholds. Extract responsibilities (e.g., auth validation, user lookup, stream validation, Strava refresh, and error formatting) into dedicated functions to improve readability and testability.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

30-30: `token_refresh` is too complex (17 > 10)

(C901)

---

49-49: Do not catch blind exception: `Exception`

(BLE001)

---

79-79: Trailing comma missing

Add trailing comma

(COM812)

---

94-94: Trailing comma missing

Add trailing comma

(COM812)

---

106-106: Abstract `raise` to an inner function

(TRY301)

---

106-106: Avoid specifying long messages outside the exception class

(TRY003)

---

112-112: Abstract `raise` to an inner function

(TRY301)

---

112-112: Avoid specifying long messages outside the exception class

(TRY003)

---

123-123: Abstract `raise` to an inner function

(TRY301)

---

123-123: Avoid specifying long messages outside the exception class

(TRY003)

---

138-138: Trailing comma missing

Add trailing comma

(COM812)

---

161-161: Trailing comma missing

Add trailing comma

(COM812)

---

184-184: Trailing comma missing

Add trailing comma

(COM812)

---

203-203: Trailing comma missing

Add trailing comma

(COM812)

---

224-224: Trailing comma missing

Add trailing comma

(COM812)

---

244-244: Trailing comma missing

Add trailing comma

(COM812)

---

265-265: Trailing comma missing

Add trailing comma

(COM812)

---

274-274: Trailing comma missing

Add trailing comma

(COM812)

---

279-279: Trailing comma missing

Add trailing comma

(COM812)

---

288-288: Trailing comma missing

Add trailing comma

(COM812)

---

293-293: Trailing comma missing

Add trailing comma

(COM812)

---

300-300: Do not catch blind exception: `Exception`

(BLE001)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0b4530a7df3ecfa4d37ad28218eeb8135b46df9d and 0bc036c2198bd58f2e9f20f695151e42f07c9f0e.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `poetry.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `.github/workflows/ci.yaml` (0 hunks)
* `.github/workflows/dev-deploy.yaml` (4 hunks)
* `Makefile` (2 hunks)
* `cloud_functions/shared/__init__.py` (1 hunks)
* `cloud_functions/shared/cloud_utils.py` (1 hunks)
* `cloud_functions/shared/logger_utils.py` (1 hunks)
* `cloud_functions/shared/task_utils.py` (1 hunks)
* `cloud_functions/token_refresh/main.py` (1 hunks)
* `cloud_functions/token_refresh/streams/strava.py` (1 hunks)
* `pyproject.toml` (1 hunks)
* `tests/cloud_functions/testing_utils.py` (2 hunks)
* `tests/cloud_functions/token_refresh/streams/__init__.py` (1 hunks)
* `tests/cloud_functions/token_refresh/streams/test_strava.py` (1 hunks)
* `tests/cloud_functions/token_refresh/test_main.py` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* .github/workflows/ci.yaml

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>cloud_functions/shared/cloud_utils.py (1)</summary><blockquote>

<details>
<summary>cloud_functions/shared/logger_utils.py (1)</summary>

* `create_structured_logger` (61-70)

</details>

</blockquote></details>
<details>
<summary>tests/cloud_functions/token_refresh/test_main.py (3)</summary><blockquote>

<details>
<summary>cloud_functions/token_refresh/main.py (1)</summary>

* `token_refresh` (30-314)

</details>
<details>
<summary>tests/cloud_functions/testing_utils.py (1)</summary>

* `populate_inmemory_firestore_with_users_and_streams` (164-226)

</details>
<details>
<summary>tests/cloud_functions/token_refresh/conftest.py (2)</summary>

* `mock_request` (19-21)
* `mock_decoded_token` (25-27)

</details>

</blockquote></details>
<details>
<summary>cloud_functions/token_refresh/main.py (4)</summary><blockquote>

<details>
<summary>cloud_functions/shared/logger_utils.py (1)</summary>

* `create_structured_logger` (61-70)

</details>
<details>
<summary>cloud_functions/token_refresh/streams/strava.py (1)</summary>

* `strava_refresh_oauth_token` (19-73)

</details>
<details>
<summary>tests/cloud_functions/token_refresh/conftest.py (1)</summary>

* `get_json` (13-15)

</details>
<details>
<summary>tests/cloud_functions/testing_utils.py (5)</summary>

* `get` (59-67)
* `collection` (69-78)
* `collection` (134-143)
* `document` (93-106)
* `to_dict` (29-35)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>cloud_functions/shared/task_utils.py</summary>

26-29: Use ternary operator `key_bytes = key.encode("utf-8") if isinstance(key, str) else key` instead of `if`-`else`-block

Replace `if`-`else`-block with `key_bytes = key.encode("utf-8") if isinstance(key, str) else key`

(SIM108)

---

42-42: Trailing comma missing

Add trailing comma

(COM812)

</details>
<details>
<summary>cloud_functions/shared/cloud_utils.py</summary>

17-17: Avoid specifying long messages outside the exception class

(TRY003)

---

25-25: Trailing comma missing

Add trailing comma

(COM812)

</details>
<details>
<summary>tests/cloud_functions/token_refresh/streams/test_strava.py</summary>

15-15: Trailing comma missing

Add trailing comma

(COM812)

</details>
<details>
<summary>cloud_functions/token_refresh/streams/strava.py</summary>

20-20: Trailing comma missing

Add trailing comma

(COM812)

---

43-43: Avoid specifying long messages outside the exception class

(TRY003)

---

55-55: Trailing comma missing

Add trailing comma

(COM812)

---

58-58: Trailing comma missing

Add trailing comma

(COM812)

---

79-79: Trailing comma missing

Add trailing comma

(COM812)

---

111-111: Trailing comma missing

Add trailing comma

(COM812)

</details>
<details>
<summary>tests/cloud_functions/token_refresh/test_main.py</summary>

15-15: Unused lambda argument: `a`

(ARG005)

---

15-15: Unused lambda argument: `kw`

(ARG005)

---

19-19: Unused lambda argument: `a`

(ARG005)

---

19-19: Unused lambda argument: `kw`

(ARG005)

---

22-22: Missing return type annotation for public function `test_token_refresh_success`

Add return type annotation: `None`

(ANN201)

---

22-22: Missing type annotation for function argument `mock_verify`

(ANN001)

---

22-22: Missing type annotation for function argument `mock_request`

(ANN001)

---

22-22: Missing type annotation for function argument `mock_decoded_token`

(ANN001)

---

30-37: Use a single `with` statement with multiple contexts instead of nested `with` statements

Combine `with` statements

(SIM117)

---

48-48: Unused lambda argument: `a`

(ARG005)

---

48-48: Unused lambda argument: `kw`

(ARG005)

---

52-52: Unused lambda argument: `a`

(ARG005)

---

52-52: Unused lambda argument: `kw`

(ARG005)

---

55-55: Missing return type annotation for public function `test_token_refresh_missing_data_source`

Add return type annotation: `None`

(ANN201)

---

56-56: Missing type annotation for function argument `mock_verify`

(ANN001)

---

56-56: Missing type annotation for function argument `mock_request`

(ANN001)

---

56-56: Missing type annotation for function argument `mock_decoded_token`

(ANN001)

---

56-56: Trailing comma missing

Add trailing comma

(COM812)

---

66-73: Use a single `with` statement with multiple contexts instead of nested `with` statements

Combine `with` statements

(SIM117)

---

85-85: Unused lambda argument: `a`

(ARG005)

---

85-85: Unused lambda argument: `kw`

(ARG005)

---

89-89: Unused lambda argument: `a`

(ARG005)

---

89-89: Unused lambda argument: `kw`

(ARG005)

---

92-92: Missing return type annotation for public function `test_token_refresh_invalid_token`

Add return type annotation: `None`

(ANN201)

---

92-92: Missing type annotation for function argument `mock_verify`

(ANN001)

---

92-92: Missing type annotation for function argument `mock_request`

(ANN001)

---

92-92: Missing type annotation for function argument `mock_decoded_token`

(ANN001)

---

103-110: Use a single `with` statement with multiple contexts instead of nested `with` statements

Combine `with` statements

(SIM117)

</details>
<details>
<summary>cloud_functions/shared/logger_utils.py</summary>

33-33: Trailing comma missing

Add trailing comma

(COM812)

</details>
<details>
<summary>cloud_functions/token_refresh/main.py</summary>

28-28: Trailing comma missing

Add trailing comma

(COM812)

---

30-30: `token_refresh` is too complex (17 > 10)

(C901)

---

49-49: Do not catch blind exception: `Exception`

(BLE001)

---

79-79: Trailing comma missing

Add trailing comma

(COM812)

---

94-94: Trailing comma missing

Add trailing comma

(COM812)

---

106-106: Abstract `raise` to an inner function

(TRY301)

---

106-106: Avoid specifying long messages outside the exception class

(TRY003)

---

112-112: Abstract `raise` to an inner function

(TRY301)

---

112-112: Avoid specifying long messages outside the exception class

(TRY003)

---

123-123: Abstract `raise` to an inner function

(TRY301)

---

123-123: Avoid specifying long messages outside the exception class

(TRY003)

---

138-138: Trailing comma missing

Add trailing comma

(COM812)

---

161-161: Trailing comma missing

Add trailing comma

(COM812)

---

184-184: Trailing comma missing

Add trailing comma

(COM812)

---

203-203: Trailing comma missing

Add trailing comma

(COM812)

---

224-224: Trailing comma missing

Add trailing comma

(COM812)

---

244-244: Trailing comma missing

Add trailing comma

(COM812)

---

265-265: Trailing comma missing

Add trailing comma

(COM812)

---

274-274: Trailing comma missing

Add trailing comma

(COM812)

---

279-279: Trailing comma missing

Add trailing comma

(COM812)

---

288-288: Trailing comma missing

Add trailing comma

(COM812)

---

293-293: Trailing comma missing

Add trailing comma

(COM812)

---

300-300: Do not catch blind exception: `Exception`

(BLE001)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (14)</summary><blockquote>

<details>
<summary>cloud_functions/shared/__init__.py (1)</summary>

`1-1`: **Approve package initializer.**

The module-level docstring and presence of `__init__.py` correctly initialize the shared utilities package for cloud functions.

</details>
<details>
<summary>tests/cloud_functions/token_refresh/streams/__init__.py (1)</summary>

`1-1`: **Approve package initializer.**

The `__init__.py` module-level docstring correctly marks the `streams` directory as a package for token refresh tests.

</details>
<details>
<summary>cloud_functions/shared/cloud_utils.py (1)</summary>

`1-33`: **Approve reformatting.**

The file has been reformatted without functional changes; the `get_secret` implementation remains correct, properly handles missing `PROJECT_ID`, and logs structured messages on success or failure.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

17-17: Avoid specifying long messages outside the exception class

(TRY003)

---

25-25: Trailing comma missing

Add trailing comma

(COM812)

</details>

</details>

</details>
<details>
<summary>tests/cloud_functions/token_refresh/streams/test_strava.py (1)</summary>

`1-37`: **Approve test formatting changes.**

The refactor only involved whitespace and formatting; test logic remains correct and continues to validate `strava_update_user_tokens` as expected.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

15-15: Trailing comma missing

Add trailing comma

(COM812)

</details>

</details>

</details>
<details>
<summary>cloud_functions/shared/task_utils.py (1)</summary>

`1-57`: **The encryption implementation looks solid.**

The implementation follows best practices for AES-256-CBC encryption with proper IV generation, PKCS#7 padding, and base64 encoding. The key handling is also properly implemented to ensure it's exactly 32 bytes.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

26-29: Use ternary operator `key_bytes = key.encode("utf-8") if isinstance(key, str) else key` instead of `if`-`else`-block

Replace `if`-`else`-block with `key_bytes = key.encode("utf-8") if isinstance(key, str) else key`

(SIM108)

---

42-42: Trailing comma missing

Add trailing comma

(COM812)

</details>

</details>

</details>
<details>
<summary>tests/cloud_functions/testing_utils.py (1)</summary>

`4-4`: **Good improvement using datetime.now(timezone.utc).**

The change from `datetime.now().replace(tzinfo=datetime.timezone.utc)` to `datetime.now(timezone.utc)` is a better approach for creating timezone-aware datetime objects. This method is more direct and follows Python best practices for working with datetime objects.



Also applies to: 186-186

</details>
<details>
<summary>tests/cloud_functions/token_refresh/test_main.py (1)</summary>

`14-16`: **Updated mock target to structured_logger.**

The updated patches target the new `structured_logger` function instead of the previous `partial_log_structured`. This aligns with changes in the main implementation.



Also applies to: 47-49, 84-86

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

15-15: Unused lambda argument: `a`

(ARG005)

---

15-15: Unused lambda argument: `kw`

(ARG005)

</details>

</details>

</details>
<details>
<summary>cloud_functions/token_refresh/streams/strava.py (1)</summary>

`10-15`: **Import structure looks good.**

The imports are well-organized with standard library imports first, followed by third-party libraries, and then local modules. The relative imports for shared utilities are properly structured.

</details>
<details>
<summary>pyproject.toml (2)</summary>

`10-10`: **Allow patch updates for Python by using a caret version**

Switching from an exact version to `python = "^3.12.2"` helps ensure you automatically receive patch releases without breaking changes.

---

`16-16`: **Verify compatibility of the numpy version downgrade**

You’ve downgraded `numpy` from `^2.2.2` to `^1.26.0`. Please confirm that all existing code paths remain compatible and that no features from newer major versions are required.

</details>
<details>
<summary>.github/workflows/dev-deploy.yaml (4)</summary>

`28-32`: **Approve introduction of the dedicated `testing` job**

Splitting out a `testing` job for checkout, Python setup, Poetry install, lint checks, shared‐code copy, and pytest creates a clearer CI structure and provides faster feedback loops.

---

`45-51`: **Cache Poetry virtual environment to speed up CI**

Caching `.venv` keyed on `poetry.lock` is an effective way to avoid reinstalling dependencies on each run and significantly reduce CI time.

---

`74-76`: **Approve running pre-commit checks before tests**

Ensuring linting and formatting pass before executing tests catches style issues early and keeps code clean.

---

`204-204`: **Approve the `cloud_function_detect_changes` job**

Introducing a per-function change detection job (with shared-code invalidation) ensures you only deploy functions that actually changed, reducing deployment time and costs.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:51:04Z
user: login: santoshgdev
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T01:52:37Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:53:01Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>cloud_functions/shared/task_utils.py (2)</summary><blockquote>

`26-27`: **LGTM!**

The ternary operator improves readability and addresses the SIM108 Ruff issue mentioned in the previous review comments.

---

`38-40`: **LGTM!**

The trailing comma addition follows Python style conventions and addresses the COM812 Ruff issue from the previous review comments.

</blockquote></details>
<details>
<summary>tests/cloud_functions/token_refresh/test_main.py (1)</summary><blockquote>

`55-57`: _🧹 Nitpick (assertive)_

**Add trailing comma after function parameters.**

The function signature should include a trailing comma for consistency with Python style conventions.

```diff
def test_token_refresh_missing_data_source(
-    mock_verify, mock_request, mock_decoded_token
+    mock_verify, mock_request, mock_decoded_token,
):
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

56-56: Trailing comma missing

(COM812)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0bc036c2198bd58f2e9f20f695151e42f07c9f0e and 8ea1533f3db6a63667027ab989a4e43c1c1ef73c.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `cloud_functions/shared/logger_utils.py` (1 hunks)
* `cloud_functions/shared/task_utils.py` (1 hunks)
* `tests/cloud_functions/token_refresh/test_main.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>tests/cloud_functions/token_refresh/test_main.py (3)</summary><blockquote>

<details>
<summary>cloud_functions/token_refresh/main.py (1)</summary>

* `token_refresh` (30-314)

</details>
<details>
<summary>tests/cloud_functions/testing_utils.py (1)</summary>

* `populate_inmemory_firestore_with_users_and_streams` (164-226)

</details>
<details>
<summary>tests/cloud_functions/token_refresh/conftest.py (2)</summary>

* `mock_request` (19-21)
* `mock_decoded_token` (25-27)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>tests/cloud_functions/token_refresh/test_main.py</summary>

56-56: Trailing comma missing

(COM812)

---

115-115: SyntaxError: Expected an identifier, but found a keyword 'assert' that cannot be used here

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>cloud_functions/shared/logger_utils.py (1)</summary>

`32-34`: **LGTM!**

The addition of the trailing comma improves code style and fixes the Ruff COM812 issue mentioned in the previous review comments.

</details>
<details>
<summary>tests/cloud_functions/token_refresh/test_main.py (3)</summary>

`13-16`: **LGTM on the logger function name update.**

Successfully updated the patch target from `partial_log_structured` to `structured_logger` to match the implementation changes in the main module.

---

`46-49`: **LGTM on the logger function name update.**

Successfully updated the patch target from `partial_log_structured` to `structured_logger` to match the implementation changes in the main module.

---

`83-86`: **LGTM on the logger function name update.**

Successfully updated the patch target from `partial_log_structured` to `structured_logger` to match the implementation changes in the main module.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:53:56Z
user: login: santoshgdev
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T01:54:23Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:54:50Z
user: login: santoshgdev
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T01:55:55Z
user: login: santoshgdev
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T01:56:08Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:56:24Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:56:53Z
user: login: santoshgdev
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T01:56:58Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:57:45Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>tests/cloud_functions/token_refresh/test_main.py (1)</summary><blockquote>

`115-116`: **Fix duplicate assert statement causing a syntax error.**

The second `assert (` at line 116 is a duplicate of the assertion on the previous line, leading to `SyntaxError: Expected an identifier, but found a keyword 'assert'`. Remove the extra `assert (` so the test compiles.

```diff
@@ tests/cloud_functions/token_refresh/test_main.py:115,116
-                assert (
-                assert (
+                assert (
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

116-116: SyntaxError: Expected an identifier, but found a keyword 'assert' that cannot be used here

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8ea1533f3db6a63667027ab989a4e43c1c1ef73c and afe4e0c7106de335c17eb79f7b7174d9eff7e8ab.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `tests/cloud_functions/token_refresh/test_main.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>tests/cloud_functions/token_refresh/test_main.py (3)</summary><blockquote>

<details>
<summary>cloud_functions/token_refresh/main.py (1)</summary>

* `token_refresh` (30-314)

</details>
<details>
<summary>tests/cloud_functions/testing_utils.py (1)</summary>

* `populate_inmemory_firestore_with_users_and_streams` (164-226)

</details>
<details>
<summary>tests/cloud_functions/token_refresh/conftest.py (2)</summary>

* `mock_request` (19-21)
* `mock_decoded_token` (25-27)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>tests/cloud_functions/token_refresh/test_main.py</summary>

55-55: SyntaxError: Expected ')', found newline

---

56-56: SyntaxError: Expected an indented block after function definition

---

116-116: SyntaxError: Expected an identifier, but found a keyword 'assert' that cannot be used here

</details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:58:14Z
user: login: santoshgdev
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-09T01:59:26Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T01:59:46Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 7**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>.github/workflows/dev-deploy.yaml (1)</summary><blockquote>

`207-215`: _🧹 Nitpick (assertive)_

**Inconsistent PYTHONPATH settings**  
You set `PYTHONPATH` globally to the workspace, then again in the `ENV_VARS` as `/app/fitnessllm-dataplatform`. This mismatch may confuse the runtime. Please unify or remove the redundant entry.

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (3)</summary><blockquote>

<details>
<summary>tests/cloud_functions/token_refresh/test_main.py (2)</summary><blockquote>

`46-53`: **Duplicate of previous patch update**  
This stub of `structured_logger` and `strava_refresh_oauth_token` mirrors the change above.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

48-48: Unused lambda argument: `a`

(ARG005)

---

48-48: Unused lambda argument: `kw`

(ARG005)

---

52-52: Unused lambda argument: `a`

(ARG005)

---

52-52: Unused lambda argument: `kw`

(ARG005)

</details>

</details>

---

`85-92`: **Duplicate of previous patch update**  
Same no-op patch logic applied here for consistency across tests.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

87-87: Unused lambda argument: `a`

(ARG005)

---

87-87: Unused lambda argument: `kw`

(ARG005)

---

91-91: Unused lambda argument: `a`

(ARG005)

---

91-91: Unused lambda argument: `kw`

(ARG005)

</details>

</details>

</blockquote></details>
<details>
<summary>.github/workflows/dev-deploy.yaml (1)</summary><blockquote>

`299-307`: **Duplicate of shared-code sync**  
This copy loop repeats the earlier step in the `testing` job. Extract into a single reusable action or composite step to adhere to DRY principles.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between afe4e0c7106de335c17eb79f7b7174d9eff7e8ab and dfa3142c938569b27c4f465164a815c7c86b3a02.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `.github/workflows/dev-deploy.yaml` (4 hunks)
* `cloud_functions/token_refresh/streams/strava.py` (1 hunks)
* `tests/cloud_functions/token_refresh/test_main.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>.github/workflows/dev-deploy.yaml (2)</summary>

```
Learnt from: santoshgdev
PR: santoshgdev/fitnessllm-dataplatform#24
File: .github/workflows/dev-deploy.yaml:70-70
Timestamp: 2025-05-09T01:56:23.068Z
Learning: For GitHub Actions cache, using a more specific restore-key like `${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-` ensures that only caches matching the current pre-commit configuration are restored, which is important for correctness. Using a more general restore-key like `${{ runner.os }}-pre-commit-` optimizes for build speed but may restore less-relevant caches.
```

```
Learnt from: santoshgdev
PR: santoshgdev/fitnessllm-dataplatform#24
File: .github/workflows/dev-deploy.yaml:71-73
Timestamp: 2025-05-09T01:57:44.299Z
Learning: When suggesting version pinning in GitHub Actions workflows, use direct version specification (e.g., `pip install package==1.2.3`) rather than attempting to use `hashFiles()`, which returns a hash of the file content, not the version number itself.
```

</details>

</details><details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>tests/cloud_functions/token_refresh/test_main.py (3)</summary><blockquote>

<details>
<summary>cloud_functions/token_refresh/main.py (1)</summary>

* `token_refresh` (30-314)

</details>
<details>
<summary>tests/cloud_functions/testing_utils.py (1)</summary>

* `populate_inmemory_firestore_with_users_and_streams` (164-226)

</details>
<details>
<summary>tests/cloud_functions/token_refresh/conftest.py (2)</summary>

* `mock_request` (19-21)
* `mock_decoded_token` (25-27)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>cloud_functions/token_refresh/streams/strava.py</summary>

45-45: Avoid specifying long messages outside the exception class

(TRY003)

---

117-117: Trailing comma missing

Add trailing comma

(COM812)

</details>
<details>
<summary>tests/cloud_functions/token_refresh/test_main.py</summary>

15-15: Unused lambda argument: `a`

(ARG005)

---

15-15: Unused lambda argument: `kw`

(ARG005)

---

19-19: Unused lambda argument: `a`

(ARG005)

---

19-19: Unused lambda argument: `kw`

(ARG005)

---

22-22: Missing return type annotation for public function `test_token_refresh_success`

Add return type annotation: `None`

(ANN201)

---

22-22: Missing type annotation for function argument `mock_verify`

(ANN001)

---

22-22: Missing type annotation for function argument `mock_request`

(ANN001)

---

22-22: Missing type annotation for function argument `mock_decoded_token`

(ANN001)

---

30-37: Use a single `with` statement with multiple contexts instead of nested `with` statements

Combine `with` statements

(SIM117)

---

48-48: Unused lambda argument: `a`

(ARG005)

---

48-48: Unused lambda argument: `kw`

(ARG005)

---

52-52: Unused lambda argument: `a`

(ARG005)

---

52-52: Unused lambda argument: `kw`

(ARG005)

---

55-55: Missing return type annotation for public function `test_token_refresh_missing_data_source`

Add return type annotation: `None`

(ANN201)

---

56-56: Missing type annotation for function argument `mock_verify`

(ANN001)

---

57-57: Missing type annotation for function argument `mock_request`

(ANN001)

---

58-58: Missing type annotation for function argument `mock_decoded_token`

(ANN001)

---

68-75: Use a single `with` statement with multiple contexts instead of nested `with` statements

Combine `with` statements

(SIM117)

---

87-87: Unused lambda argument: `a`

(ARG005)

---

87-87: Unused lambda argument: `kw`

(ARG005)

---

91-91: Unused lambda argument: `a`

(ARG005)

---

91-91: Unused lambda argument: `kw`

(ARG005)

---

94-94: Missing return type annotation for public function `test_token_refresh_invalid_token`

Add return type annotation: `None`

(ANN201)

---

94-94: Missing type annotation for function argument `mock_verify`

(ANN001)

---

94-94: Missing type annotation for function argument `mock_request`

(ANN001)

---

94-94: Missing type annotation for function argument `mock_decoded_token`

(ANN001)

---

105-112: Use a single `with` statement with multiple contexts instead of nested `with` statements

Combine `with` statements

(SIM117)

</details>

</details>
<details>
<summary>🪛 actionlint (1.7.4)</summary>

<details>
<summary>.github/workflows/dev-deploy.yaml</summary>

98-98: the runner of "codecov/codecov-action@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>cloud_functions/token_refresh/streams/strava.py (2)</summary>

`18-23`: **Approved: Trailing commas in function signature**  
The added trailing comma after `refresh_token: str` aligns with style guidelines for multi-line function signatures and enhances maintainability.

---

`55-63`: **Approved: Multi-line dict formatting**  
The refactored `new_tokens` dictionary uses clean multi-line formatting with trailing commas for each entry, improving readability and diff-friendliness.

</details>
<details>
<summary>tests/cloud_functions/token_refresh/test_main.py (2)</summary>

`13-20`: **Approved: Stub external calls and suppress logging**  
Replacing previous patches with no-op lambdas for `structured_logger` and `strava_refresh_oauth_token` isolates the HTTP handler tests from side effects and external dependencies.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

15-15: Unused lambda argument: `a`

(ARG005)

---

15-15: Unused lambda argument: `kw`

(ARG005)

---

19-19: Unused lambda argument: `a`

(ARG005)

---

19-19: Unused lambda argument: `kw`

(ARG005)

</details>

</details>

---

`115-119`: **Approved: Corrected assertion message**  
The test now checks for `"Invalid Firebase ID Token; JWT Token Issue"`, matching the updated error message in production code.

</details>
<details>
<summary>.github/workflows/dev-deploy.yaml (3)</summary>

`67-70`: **Approved: Precise pre-commit cache key**  
Using `${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-` as the restore key balances correctness and cache hit rates.

---

`131-135`: **Verify change detection patterns**  
Including `.github/` in the grep for relevant changes will redeploy on any workflow file update. Please confirm that triggering a deploy on workflow changes is the intended behavior.

---

`223-229`: **Approved: Fine-grained function-level deploy detection**  
The new `cloud_function_detect_changes` job efficiently sets outputs for each function based on file diffs, enabling targeted deployments.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T02:14:17Z

Review comments:
diff_hunk: @@ -1,70 +1,70 @@
-"""Logging utilities."""
-import json
-import logging
-from functools import partial
-from logging import Logger
-from typing import Callable
-
-from beartype.typing import Optional
-
-
-def setup_logger(name: Optional[str] = None, level: int = logging.DEBUG) -> Logger:
-    """Sets up a logger with a console handler.
-
-    Args:
-        name (str): Name of the logger.
-        level (int): Logging level (e.g., logging.DEBUG, logging.INFO).
-
-    Returns:
-        logging.Logger: Configured logger instance.
-    """
-    # Create a custom logger
-    logger = logging.getLogger(name)
-    logger.setLevel(level)
-
-    # Prevent adding duplicate handlers if this function is called multiple times
-    if logger.hasHandlers():
-        return logger
-
-    console_handler = logging.StreamHandler()
-    console_handler.setLevel(level)
-
-    formatter = logging.Formatter(
-        "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
-    )
-    console_handler.setFormatter(formatter)
-
-    logger.addHandler(console_handler)
-
-    return logger
-
-
-logger = setup_logger()
-
-
-def log_structured(function_name: str, message: str, **kwargs: object) -> None:
-    """Helper function to log in structured JSON format.
-
-    Args:
-        function_name: Name of the function generating the log
-        message: The main log message
-        **kwargs: Additional key-value pairs to include in the log
-    """
-    log_data = {
-        "function": function_name,
-        "message": f"{function_name}-{message}",
-        **kwargs,
-    }
-    logger.info(json.dumps(log_data))
-
-
-def create_structured_logger(function_name: str) -> Callable:
-    """Create a structured logger for a specific function.
-
-    Args:
-        function_name (str): The name of the function to log.
-
-    Returns:
-        Callable: A partial function that logs in a structured format.
-    """
-    return partial(log_structured, function_name=function_name)
+"""Logging utilities."""
+import json
+import logging
+from functools import partial
+from logging import Logger
+from typing import Callable
+
+from beartype.typing import Optional
+
+
+def setup_logger(name: Optional[str] = None, level: int = logging.DEBUG) -> Logger:
+    """Sets up a logger with a console handler.
+
+    Args:
+        name (str): Name of the logger.
+        level (int): Logging level (e.g., logging.DEBUG, logging.INFO).
+
+    Returns:
+        logging.Logger: Configured logger instance.
+    """
+    # Create a custom logger
+    logger = logging.getLogger(name)
+    logger.setLevel(level)
+
+    # Prevent adding duplicate handlers if this function is called multiple times
+    if logger.hasHandlers():
+        return logger
+
+    console_handler = logging.StreamHandler()
+    console_handler.setLevel(level)
+
+    formatter = logging.Formatter(
+        "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+    )
path: cloud_functions/shared/logger_utils.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Add trailing comma in multi-line `Formatter` call.**

Per Ruff (COM812), include a trailing comma after the format string to conform to multi-line argument formatting:

```diff
 formatter = logging.Formatter(
-    "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+    "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
 )
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    formatter = logging.Formatter(
        "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

33-33: Trailing comma missing

Add trailing comma

(COM812)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,56 +1,56 @@
-"""Task utils."""
-import base64
-import os
-
-from beartype import beartype
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives import padding
-from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
-
-
-@beartype
-def encrypt_token(token: str, key: str) -> str:
-    """Encrypt a token using AES-256-CBC with PKCS#7 padding.
-
-    Args:
-        token (str): The plaintext token to be encrypted.
-        key (str): The encryption key. This key will be encoded as UTF-8 and adjusted to 32 bytes.
-
-    Returns:
-        str: The encrypted token in the format "iv:encrypted", where both the IV and the ciphertext are base64-encoded.
-    """
-    # Convert the plaintext token to bytes.
-    token_bytes = token.encode("utf-8")
-
-    # Convert the key to bytes if necessary.
-    if isinstance(key, str):
-        key_bytes = key.encode("utf-8")
-    else:
-        key_bytes = key
-
-    # Ensure the key is exactly 32 bytes long (AES-256 requirement).
-    if len(key_bytes) < 32:
-        key_bytes = key_bytes.ljust(32, b"\0")  # Pad with zeros.
-    elif len(key_bytes) > 32:
-        key_bytes = key_bytes[:32]  # Truncate if longer than 32 bytes.
-
-    # Generate a random 16-byte initialization vector (IV).
-    iv = os.urandom(16)
-
-    # Apply PKCS#7 padding to the plaintext token.
-    padder = padding.PKCS7(
-        128
-    ).padder()  # The block size is 128 bits (16 bytes) for AES.
-    padded_data = padder.update(token_bytes) + padder.finalize()
-
-    # Create the AES cipher in CBC mode.
-    cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=default_backend())
-    encryptor = cipher.encryptor()
-    encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
-
-    # Base64-encode the IV and encrypted data.
-    iv_encoded = base64.b64encode(iv).decode("utf-8")
-    encrypted_encoded = base64.b64encode(encrypted_data).decode("utf-8")
-
-    # Return the combined result in the format "iv:encrypted".
-    return f"{iv_encoded}:{encrypted_encoded}"
+"""Task utils."""
+import base64
+import os
+
+from beartype import beartype
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import padding
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+
+
+@beartype
+def encrypt_token(token: str, key: str) -> str:
+    """Encrypt a token using AES-256-CBC with PKCS#7 padding.
+
+    Args:
+        token (str): The plaintext token to be encrypted.
+        key (str): The encryption key. This key will be encoded as UTF-8 and adjusted to 32 bytes.
+
+    Returns:
+        str: The encrypted token in the format "iv:encrypted", where both the IV and the ciphertext are base64-encoded.
+    """
+    # Convert the plaintext token to bytes.
+    token_bytes = token.encode("utf-8")
+
+    # Convert the key to bytes if necessary.
+    if isinstance(key, str):
+        key_bytes = key.encode("utf-8")
+    else:
+        key_bytes = key
+
path: cloud_functions/shared/task_utils.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consider using a ternary operator for improved readability.**

The conditional assignment of `key_bytes` can be simplified for better readability.

```diff
-    # Convert the key to bytes if necessary.
-    if isinstance(key, str):
-        key_bytes = key.encode("utf-8")
-    else:
-        key_bytes = key
+    # Convert the key to bytes if necessary.
+    key_bytes = key.encode("utf-8") if isinstance(key, str) else key
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    # Convert the key to bytes if necessary.
    key_bytes = key.encode("utf-8") if isinstance(key, str) else key
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

26-29: Use ternary operator `key_bytes = key.encode("utf-8") if isinstance(key, str) else key` instead of `if`-`else`-block

Replace `if`-`else`-block with `key_bytes = key.encode("utf-8") if isinstance(key, str) else key`

(SIM108)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,56 +1,56 @@
-"""Task utils."""
-import base64
-import os
-
-from beartype import beartype
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives import padding
-from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
-
-
-@beartype
-def encrypt_token(token: str, key: str) -> str:
-    """Encrypt a token using AES-256-CBC with PKCS#7 padding.
-
-    Args:
-        token (str): The plaintext token to be encrypted.
-        key (str): The encryption key. This key will be encoded as UTF-8 and adjusted to 32 bytes.
-
-    Returns:
-        str: The encrypted token in the format "iv:encrypted", where both the IV and the ciphertext are base64-encoded.
-    """
-    # Convert the plaintext token to bytes.
-    token_bytes = token.encode("utf-8")
-
-    # Convert the key to bytes if necessary.
-    if isinstance(key, str):
-        key_bytes = key.encode("utf-8")
-    else:
-        key_bytes = key
-
-    # Ensure the key is exactly 32 bytes long (AES-256 requirement).
-    if len(key_bytes) < 32:
-        key_bytes = key_bytes.ljust(32, b"\0")  # Pad with zeros.
-    elif len(key_bytes) > 32:
-        key_bytes = key_bytes[:32]  # Truncate if longer than 32 bytes.
-
-    # Generate a random 16-byte initialization vector (IV).
-    iv = os.urandom(16)
-
-    # Apply PKCS#7 padding to the plaintext token.
-    padder = padding.PKCS7(
-        128
-    ).padder()  # The block size is 128 bits (16 bytes) for AES.
-    padded_data = padder.update(token_bytes) + padder.finalize()
-
-    # Create the AES cipher in CBC mode.
-    cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=default_backend())
-    encryptor = cipher.encryptor()
-    encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
-
-    # Base64-encode the IV and encrypted data.
-    iv_encoded = base64.b64encode(iv).decode("utf-8")
-    encrypted_encoded = base64.b64encode(encrypted_data).decode("utf-8")
-
-    # Return the combined result in the format "iv:encrypted".
-    return f"{iv_encoded}:{encrypted_encoded}"
+"""Task utils."""
+import base64
+import os
+
+from beartype import beartype
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import padding
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+
+
+@beartype
+def encrypt_token(token: str, key: str) -> str:
+    """Encrypt a token using AES-256-CBC with PKCS#7 padding.
+
+    Args:
+        token (str): The plaintext token to be encrypted.
+        key (str): The encryption key. This key will be encoded as UTF-8 and adjusted to 32 bytes.
+
+    Returns:
+        str: The encrypted token in the format "iv:encrypted", where both the IV and the ciphertext are base64-encoded.
+    """
+    # Convert the plaintext token to bytes.
+    token_bytes = token.encode("utf-8")
+
+    # Convert the key to bytes if necessary.
+    if isinstance(key, str):
+        key_bytes = key.encode("utf-8")
+    else:
+        key_bytes = key
+
+    # Ensure the key is exactly 32 bytes long (AES-256 requirement).
+    if len(key_bytes) < 32:
+        key_bytes = key_bytes.ljust(32, b"\0")  # Pad with zeros.
+    elif len(key_bytes) > 32:
+        key_bytes = key_bytes[:32]  # Truncate if longer than 32 bytes.
+
+    # Generate a random 16-byte initialization vector (IV).
+    iv = os.urandom(16)
+
+    # Apply PKCS#7 padding to the plaintext token.
+    padder = padding.PKCS7(
+        128
+    ).padder()  # The block size is 128 bits (16 bytes) for AES.
+    padded_data = padder.update(token_bytes) + padder.finalize()
path: cloud_functions/shared/task_utils.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Add trailing comma for consistency.**

Adding a trailing comma after the value `128` would maintain consistency with Python style conventions and make future additions easier.

```diff
    # Apply PKCS#7 padding to the plaintext token.
    padder = padding.PKCS7(
-        128
+        128,
    ).padder()  # The block size is 128 bits (16 bytes) for AES.
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    padder = padding.PKCS7(
        128,
    ).padder()  # The block size is 128 bits (16 bytes) for AES.
    padded_data = padder.update(token_bytes) + padder.finalize()
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

42-42: Trailing comma missing

Add trailing comma

(COM812)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,117 +1,117 @@
-import json
-from unittest.mock import patch
-
-import flask
-from firebase_admin import auth
-
-from cloud_functions.token_refresh.main import token_refresh
-from tests.cloud_functions.testing_utils import (
-    populate_inmemory_firestore_with_users_and_streams,
-)
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                response = token_refresh(mock_request)
-                assert response.status_code == 200
-                assert (
-                    "Token refreshed successfully for Strava"
-                    in json.loads(response.response[0])["message"]
-                )
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_missing_data_source(
-    mock_verify, mock_request, mock_decoded_token
-):
-    """Test token refresh with missing data source."""
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                mock_request.args = {}
-                response = token_refresh(mock_request)
-                assert response.status_code == 400
-                assert (
-                    "Required data_source parameter is missing"
-                    in json.loads(response.response[0])["message"]
-                )
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_invalid_token(mock_verify, mock_request, mock_decoded_token):
-    """Test token refresh with invalid token."""
-
-    # Setup in-memory Firestore with one user and strava stream
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                mock_verify.side_effect = auth.InvalidIdTokenError("Invalid token")
-                response = token_refresh(mock_request)
-                assert response.status_code == 401
-                assert (
-                    "Invalid Firebase ID Token; JWT Token Issu"
-                    in json.loads(response.response[0])["message"]
-                )
+import json
+from unittest.mock import patch
+
+import flask
+from firebase_admin import auth
+
+from cloud_functions.token_refresh.main import token_refresh
+from tests.cloud_functions.testing_utils import (
+    populate_inmemory_firestore_with_users_and_streams,
+)
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                response = token_refresh(mock_request)
+                assert response.status_code == 200
+                assert (
+                    "Token refreshed successfully for Strava"
+                    in json.loads(response.response[0])["message"]
+                )
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_missing_data_source(
+    mock_verify, mock_request, mock_decoded_token
+):
+    """Test token refresh with missing data source."""
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                mock_request.args = {}
+                response = token_refresh(mock_request)
+                assert response.status_code == 400
+                assert (
+                    "Required data_source parameter is missing"
+                    in json.loads(response.response[0])["message"]
+                )
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_invalid_token(mock_verify, mock_request, mock_decoded_token):
+    """Test token refresh with invalid token."""
+
+    # Setup in-memory Firestore with one user and strava stream
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                mock_verify.side_effect = auth.InvalidIdTokenError("Invalid token")
+                response = token_refresh(mock_request)
+                assert response.status_code == 401
+                assert (
+                    "Invalid Firebase ID Token; JWT Token Issu"
+                    in json.loads(response.response[0])["message"]
+                )
path: tests/cloud_functions/token_refresh/test_main.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix typo in assertion message.**

There appears to be a typo in the assertion message - "Issu" should be "Issue".

```diff
                assert (
-                    "Invalid Firebase ID Token; JWT Token Issu"
+                    "Invalid Firebase ID Token; JWT Token Issue"
                    in json.loads(response.response[0])["message"]
                )
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                assert (
                    "Invalid Firebase ID Token; JWT Token Issue"
                    in json.loads(response.response[0])["message"]
                )
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,117 +1,117 @@
-import json
-from unittest.mock import patch
-
-import flask
-from firebase_admin import auth
-
-from cloud_functions.token_refresh.main import token_refresh
-from tests.cloud_functions.testing_utils import (
-    populate_inmemory_firestore_with_users_and_streams,
-)
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                response = token_refresh(mock_request)
-                assert response.status_code == 200
-                assert (
-                    "Token refreshed successfully for Strava"
-                    in json.loads(response.response[0])["message"]
-                )
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_missing_data_source(
-    mock_verify, mock_request, mock_decoded_token
-):
-    """Test token refresh with missing data source."""
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                mock_request.args = {}
-                response = token_refresh(mock_request)
-                assert response.status_code == 400
-                assert (
-                    "Required data_source parameter is missing"
-                    in json.loads(response.response[0])["message"]
-                )
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_invalid_token(mock_verify, mock_request, mock_decoded_token):
-    """Test token refresh with invalid token."""
-
-    # Setup in-memory Firestore with one user and strava stream
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                mock_verify.side_effect = auth.InvalidIdTokenError("Invalid token")
-                response = token_refresh(mock_request)
-                assert response.status_code == 401
-                assert (
-                    "Invalid Firebase ID Token; JWT Token Issu"
-                    in json.loads(response.response[0])["message"]
-                )
+import json
+from unittest.mock import patch
+
+import flask
+from firebase_admin import auth
+
+from cloud_functions.token_refresh.main import token_refresh
+from tests.cloud_functions.testing_utils import (
+    populate_inmemory_firestore_with_users_and_streams,
+)
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                response = token_refresh(mock_request)
+                assert response.status_code == 200
+                assert (
+                    "Token refreshed successfully for Strava"
+                    in json.loads(response.response[0])["message"]
+                )
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_missing_data_source(
+    mock_verify, mock_request, mock_decoded_token
+):
path: tests/cloud_functions/token_refresh/test_main.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Add trailing comma for consistency.**

Adding a trailing comma after the last parameter in function signature would maintain consistency with Python style conventions.

```diff
def test_token_refresh_missing_data_source(
-    mock_verify, mock_request, mock_decoded_token
+    mock_verify, mock_request, mock_decoded_token,
):
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
def test_token_refresh_missing_data_source(
    mock_verify, mock_request, mock_decoded_token,
):
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

56-56: Missing type annotation for function argument `mock_verify`

(ANN001)

---

56-56: Missing type annotation for function argument `mock_request`

(ANN001)

---

56-56: Missing type annotation for function argument `mock_decoded_token`

(ANN001)

---

56-56: Trailing comma missing

Add trailing comma

(COM812)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,113 +1,113 @@
-"""Strava specific utils."""
-import os
-from os import environ
-
-from beartype import beartype
-from beartype.typing import Any, Dict
-from google.cloud import firestore
-from stravalib.client import Client
-
-from ..shared.cloud_utils import get_secret
-from ..shared.logger_utils import create_structured_logger
-from ..shared.task_utils import encrypt_token
-from ..utils.task_utils import decrypt_token, update_last_refresh
-
-structured_logger = create_structured_logger(__name__)
-
-
-@beartype
-def strava_refresh_oauth_token(
-    db: firestore.Client, uid: str, refresh_token: str
-) -> None:
-    """Call Strava OAuth to refresh the token.
-
-    Args:
-        db: Firestore client.
-        uid: Firestore user id.
-        refresh_token: Encrypted Strava OAuth refresh token.
-
-    Raises:
-        ValueError: If refresh token is invalid.
-    """
-    structured_logger(message="Starting token refresh", uid=uid)
-
-    encryption_key = get_secret(os.environ["ENCRYPTION_SECRET"])["token"]
-
-    client = Client()
-    strava_secret = get_secret(environ["STRAVA_SECRET"])
-    client_id = strava_secret.get("client_id")
-    client_secret = strava_secret.get("client_secret")
-
-    if not client_id or not client_secret:
-        structured_logger(message="Strava credentials not found", level="ERROR")
-        raise ValueError("Strava credentials not found in Secret Manager")
-
-    try:
-        token_response = client.refresh_access_token(
-            client_id=client_id,
-            client_secret=client_secret,
-            refresh_token=decrypt_token(refresh_token, encryption_key),
-        )
-        structured_logger(message="Token refresh successful", uid=uid)
-
-        new_tokens = {
-            "accessToken": encrypt_token(
-                token_response["access_token"], encryption_key
-            ),
-            "refreshToken": encrypt_token(
-                token_response["refresh_token"], encryption_key
-            ),
-            "expiresAt": token_response["expires_at"],
-            "lastTokenRefresh": update_last_refresh(),
-        }
-
-        strava_update_user_tokens(db=db, uid=uid, new_tokens=new_tokens)
-        structured_logger(message="Tokens updated in Firestore", uid=uid)
-    except Exception as e:
-        structured_logger(
-            message="Error refreshing token",
-            uid=uid,
-            error=str(e),
-            level="ERROR",
-        )
-        raise
-
-
-# Bear type is removed here due to a test that has a testing component located in tests/.
-# This is done so that the testing components don't need to be shipped with the production code.
-def strava_update_user_tokens(
-    db: firestore.Client, uid: str, new_tokens: Dict[str, Any]
-) -> None:
-    """Update user document with new tokens.
-
-    Args:
-        db: Firestore client.
-        uid: Firestore user id.
-        new_tokens: New tokens.
-    """
-    structured_logger(message="Updating user tokens", uid=uid)
-
-    strava_ref = (
-        db.collection("users").document(uid).collection("stream").document("strava")
-    )
-
-    doc = strava_ref.get()
-    if not doc.exists:
-        structured_logger(
-            message="Strava document doesn't exist in stream subcollection",
-            uid=uid,
-            level="ERROR",
-        )
-        # Create the document with default values
-        strava_ref.set(new_tokens, merge=True)
-        return
-
-    strava_ref.update(
-        {
-            "accessToken": new_tokens["accessToken"],
-            "refreshToken": new_tokens["refreshToken"],
-            "expiresAt": new_tokens["expiresAt"],
-            "lastTokenRefresh": new_tokens["lastTokenRefresh"],
-        }
-    )
-    structured_logger(message="User tokens updated successfully", uid=uid)
+"""Strava specific utils."""
+import os
+from os import environ
+
+from beartype import beartype
+from beartype.typing import Any, Dict
+from google.cloud import firestore
+from stravalib.client import Client
+
+from ..shared.cloud_utils import get_secret
+from ..shared.logger_utils import create_structured_logger
+from ..shared.task_utils import encrypt_token
+from ..utils.task_utils import decrypt_token, update_last_refresh
+
+structured_logger = create_structured_logger(__name__)
+
+
+@beartype
+def strava_refresh_oauth_token(
+    db: firestore.Client, uid: str, refresh_token: str
+) -> None:
path: cloud_functions/token_refresh/streams/strava.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Add trailing commas for consistency.**

Adding trailing commas in function parameters and dictionary items would maintain consistency with Python style conventions and make future additions easier.

Consider adding trailing commas in these locations:
1. Line 20-21: After `refresh_token: str` in function parameters
2. Line 55-56: After `token_response["access_token"], encryption_key`
3. Line 58-59: After `token_response["refresh_token"], encryption_key`
4. Line 79-80: After `new_tokens: Dict[str, Any]` in function parameters
5. Line 111-112: After `"lastTokenRefresh": new_tokens["lastTokenRefresh"]` in dictionary

Example for the first instance:
```diff
@beartype
def strava_refresh_oauth_token(
-    db: firestore.Client, uid: str, refresh_token: str
+    db: firestore.Client, uid: str, refresh_token: str,
) -> None:
```


Also applies to: 55-56, 58-59, 79-80, 111-112

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

20-20: Trailing comma missing

Add trailing comma

(COM812)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,9 +13,11 @@ clean:
 	poetry install --sync
 
 build:
+	make copy_shared
 	docker build . -t fitnessllm-dp
path: Makefile
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Use `$(MAKE)` to invoke sub-targets for consistency**

Instead of hard‐coding `make copy_shared`, replace it with the built‐in `$(MAKE)` variable so that any flags or options passed to the top‐level `make` propagate correctly:

```diff
-	make copy_shared
+	$(MAKE) copy_shared
```

Apply this change to the `build`, `test`, and `run` targets.  



Also applies to: 20-21, 32-33

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,41 +1,40 @@
-[project]
-name = "fitnessllm_dataplatform"
-version = "0.1.0"
-description = ""
-authors = [
-    { name = "Santosh G", email = "santosh@santoshg.io" }
-]
-
-[tool.poetry.dependencies]
-python = "3.12.2"
-cloudpathlib = {extras = ["gs"], version = "^0.20.0"}
-typing-extensions = "^4.12.2"
-fire = "^0.7.0"
-google-cloud-secret-manager = "^2.22.1"
-tqdm = "^4.67.1"
-numpy = "^2.2.2"
-pandas = "^2.2.3"
-google-cloud-bigquery = {extras = ["pandas"], version = "^3.29.0"}
-joblib = "^1.4.2"
-tqdm-joblib = "^0.0.4"
-google-cloud-bigquery-storage = "^2.27.0"
-stravalib = "^2.1"
-pytest = "^8.3.4"
-pre-commit = "^4.1.0"
-firebase-admin = "^6.7.0"
-redis = "^5.2.1"
-jinja2 = "^3.1.5"
-sqlglot = "^26.7.0"
-
-[tool.poetry.dev-dependencies]
-beartype = "^0.19.0"
-pytest-cov = "^6.0.0"
-firebase-functions = "^0.4.2"
-faker = "^37.1.0"
-
-[build-system]
-requires = ["poetry-core>=1.0.0"]
-build-backend = "poetry.core.masonry.api"
-
-[tool.poetry.requires-plugins]
-poetry-plugin-export = ">=1.8"
+[project]
+name = "fitnessllm_dataplatform"
+version = "0.1.0"
+description = ""
+authors = [
+    { name = "Santosh G", email = "santosh@santoshg.io" }
+]
+
+[tool.poetry.dependencies]
+python = "^3.12.2"
+cloudpathlib = {extras = ["gs"], version = "^0.20.0"}
+typing-extensions = "^4.12.2"
+fire = "^0.7.0"
+google-cloud-secret-manager = "^2.22.1"
+tqdm = "^4.67.1"
+numpy = "^1.26.0"
+pandas = "^2.2.3"
+google-cloud-bigquery = {extras = ["pandas"], version = "^3.29.0"}
+joblib = "^1.4.2"
+tqdm-joblib = "^0.0.4"
+google-cloud-bigquery-storage = "^2.27.0"
+stravalib = "^2.1"
+pytest = "^8.3.4"
+pre-commit = "^4.1.0"
+firebase-admin = "^6.7.0"
+redis = "^5.2.1"
+jinja2 = "^3.1.5"
+sqlglot = "^26.7.0"
+faker = "^37.1.0"
+flask = "^3.1.0"
+firebase-functions = "^0.4.2"
+beartype = "^0.19.0"
+pytest-cov = "^6.0.0"
path: pyproject.toml
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Reintroduce a dev-dependencies section to isolate non-runtime packages**

Moving development tools like `faker`, `flask`, `firebase-functions`, `beartype`, and `pytest-cov` into the main dependencies will bloat your production install. Best practice is to keep testing, linting, and dev utilities under `[tool.poetry.dev-dependencies]`.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-09T01:51:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,314 +1,314 @@
-"""Main Entry point for Token Refresh."""
-import json
-import traceback
-
-from firebase_admin import auth, initialize_app
-from firebase_functions import https_fn, options
-from google.cloud import firestore
-
-from .shared.logger_utils import create_structured_logger
-from .streams.strava import strava_refresh_oauth_token
-
-structured_logger = create_structured_logger(__name__)
-
-try:
-    initialize_app()
-    structured_logger(message="Firebase Admin initialized successfully")
-except Exception as e:
-    structured_logger(
-        message="Error initializing Firebase Admin",
-        error=str(e),
-        level="ERROR",
-        traceback=traceback.format_exc(),
-    )
-    raise
-
-
-@https_fn.on_request(
-    cors=options.CorsOptions(cors_origins=["*"], cors_methods=["POST", "OPTIONS"])
-)
-def token_refresh(request: https_fn.Request) -> https_fn.Response:
-    """Cloud function taking http parameters to perform update of tokens.
-
-    Args:
-        request (http request): http request.
-
-    Note: At current time, it registers the parameters uid (firebase user id) and data_source.
-    """
-    # Log all request details at the start
-    structured_logger(
-        message="Request received",
-        method=request.method,
-        headers=dict(request.headers),
-        url=request.url,
-        args=dict(request.args),
-    )
-    try:
-        body = request.get_json(silent=True)
-        structured_logger(message="Request body", body=body)
-    except Exception as e:
-        structured_logger(
-            message="Error parsing request body",
-            error=str(e),
-            level="ERROR",
-            traceback=traceback.format_exc(),
-        )
-
-    # Handle OPTIONS request for CORS preflight
-    if request.method == "OPTIONS":
-        return https_fn.Response(
-            status=204,
-            headers={
-                "Access-Control-Allow-Origin": "*",
-                "Access-Control-Allow-Methods": "POST",
-                "Access-Control-Allow-Headers": "Authorization, Content-Type",
-                "Access-Control-Max-Age": "3600",
-            },
-        )
-
-    # Get data_source from query parameters instead of body
-    data_source = request.args.get("data_source")
-    if not data_source:
-        structured_logger(message="Missing data_source parameter", level="ERROR")
-        return https_fn.Response(
-            status=400,
-            response=json.dumps(
-                {
-                    "error": "Bad Request",
-                    "message": "Required data_source parameter is missing!",
-                }
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-
-    try:
-        # Log all headers for debugging
-        all_headers = dict(request.headers)
-        structured_logger(
-            message="Received headers",
-            headers=all_headers,
-            auth_header=request.headers.get(
-                "Authorization", "No Authorization header found"
-            ),
-        )
-
-        # Get the Authorization header
-        auth_header = request.headers.get("Authorization", "")
-        if not auth_header.startswith("Bearer "):
-            structured_logger(
-                message="Invalid Authorization header",
-                received_header=auth_header,
-                level="ERROR",
-            )
-            raise auth.InvalidIdTokenError("No valid authorization header found")
-
-        # Extract the token from the Authorization header
-        token = auth_header.split("Bearer ")[1]
-        if not token:
-            structured_logger(message="Empty Bearer token", level="ERROR")
-            raise auth.InvalidIdTokenError("Empty Bearer token")
-
-        # Verify the Firebase ID token and log its contents
-        decoded_token = auth.verify_id_token(token)
-        structured_logger(
-            message="Decoded token contents",
-            token_empty=decoded_token is None or decoded_token == "",
-        )
-
-        uid = decoded_token.get("uid") or decoded_token.get("sub")
-        if not uid:
-            raise auth.InvalidIdTokenError("No uid or sub claim found in token")
-
-        structured_logger(message="Token verified", uid=uid)
-
-        db = firestore.Client()
-        doc = db.collection("users").document(uid).get()
-
-        if not doc.exists:
-            structured_logger(message="User not found", uid=uid, level="ERROR")
-            return https_fn.Response(
-                status=404,
-                response=json.dumps(
-                    {
-                        "error": "Not Found",
-                        "message": f"User {uid} does not exist in Firestore",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        stream_doc = (
-            db.collection("users")
-            .document(uid)
-            .collection("stream")
-            .document(data_source)
-            .get()
-        )
-        if not stream_doc.exists:
-            # handle error (e.g., return 404 or similar)
-            return https_fn.Response(
-                status=404,
-                response=json.dumps(
-                    {
-                        "error": "Not Found",
-                        "message": f"Stream data for user {uid} and data source {data_source} not found",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        stream_data = stream_doc.to_dict()
-
-        if not stream_data or not stream_data.get("refreshToken"):
-            structured_logger(
-                message="No refresh token found",
-                uid=uid,
-                data_source=data_source,
-                level="ERROR",
-            )
-            return https_fn.Response(
-                status=400,
-                response=json.dumps(
-                    {
-                        "error": "Bad Request",
-                        "message": f"Bad Request - No refresh token found for user {uid} and data source {data_source}",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        if data_source == "strava":
-            try:
-                strava_refresh_oauth_token(db, uid, stream_data["refreshToken"])
-                structured_logger(
-                    message="Token refresh successful",
-                    uid=uid,
-                    data_source=data_source,
-                )
-                return https_fn.Response(
-                    status=200,
-                    response=json.dumps(
-                        {"message": "Token refreshed successfully for Strava."}
-                    ),
-                    headers={
-                        "Content-Type": "application/json",
-                        "Access-Control-Allow-Origin": "*",
-                    },
-                )
-            except ValueError as e:
-                if "credentials not found" in str(e):
-                    structured_logger(
-                        message="Strava credentials not found",
-                        error=str(e),
-                        level="ERROR",
-                        traceback=traceback.format_exc(),
-                    )
-                    return https_fn.Response(
-                        status=500,
-                        response=json.dumps(
-                            {
-                                "error": "Internal Server Error",
-                                "message": "Internal Server Error - Strava credentials not found in Secret Manager",
-                            }
-                        ),
-                        headers={
-                            "Content-Type": "application/json",
-                            "Access-Control-Allow-Origin": "*",
-                        },
-                    )
-                raise
-        else:
-            structured_logger(
-                message="Unsupported data source",
-                data_source=data_source,
-                level="ERROR",
-            )
-            return https_fn.Response(
-                status=400,
-                response=json.dumps(
-                    {
-                        "error": "Bad Request",
-                        "message": f"Bad Request - Unsupported data source: {data_source}",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-    except auth.InvalidIdTokenError:
-        structured_logger(
-            message="Invalid Firebase ID Token; JWT Token Issue",
-            level="ERROR",
-            auth=auth_header,
-            traceback=traceback.format_exc(),
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {
-                    "error": "Invalid Token",
-                    "message": "Invalid Firebase ID Token; JWT Token Issue",
-                }
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except auth.ExpiredIdTokenError:
-        structured_logger(
-            message="Expired token", level="ERROR", traceback=traceback.format_exc()
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {"error": "Unauthorized", "message": "Unauthorized - Expired token"}
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except auth.RevokedIdTokenError:
-        structured_logger(
-            message="Revoked token", level="ERROR", traceback=traceback.format_exc()
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {"error": "Unauthorized", "message": "Unauthorized - Revoked token"}
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except Exception as e:
-        structured_logger(
-            message="Error in token refresh",
-            error=str(e),
-            level="ERROR",
-            traceback=traceback.format_exc(),
-        )
-        return https_fn.Response(
-            status=500,
-            response=json.dumps({"error": "Internal Server Error", "message": str(e)}),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
+"""Main Entry point for Token Refresh."""
+import json
+import traceback
+
+from firebase_admin import auth, initialize_app
+from firebase_functions import https_fn, options
+from google.cloud import firestore
+
+from .shared.logger_utils import create_structured_logger
+from .streams.strava import strava_refresh_oauth_token
+
+structured_logger = create_structured_logger(__name__)
+
+try:
+    initialize_app()
+    structured_logger(message="Firebase Admin initialized successfully")
+except Exception as e:
+    structured_logger(
+        message="Error initializing Firebase Admin",
+        error=str(e),
+        level="ERROR",
+        traceback=traceback.format_exc(),
+    )
+    raise
+
+
+@https_fn.on_request(
+    cors=options.CorsOptions(cors_origins=["*"], cors_methods=["POST", "OPTIONS"])
+)
+def token_refresh(request: https_fn.Request) -> https_fn.Response:
+    """Cloud function taking http parameters to perform update of tokens.
+
+    Args:
+        request (http request): http request.
+
+    Note: At current time, it registers the parameters uid (firebase user id) and data_source.
+    """
+    # Log all request details at the start
+    structured_logger(
+        message="Request received",
+        method=request.method,
+        headers=dict(request.headers),
+        url=request.url,
+        args=dict(request.args),
+    )
+    try:
+        body = request.get_json(silent=True)
+        structured_logger(message="Request body", body=body)
+    except Exception as e:
+        structured_logger(
+            message="Error parsing request body",
+            error=str(e),
+            level="ERROR",
+            traceback=traceback.format_exc(),
+        )
+
+    # Handle OPTIONS request for CORS preflight
+    if request.method == "OPTIONS":
+        return https_fn.Response(
+            status=204,
+            headers={
+                "Access-Control-Allow-Origin": "*",
+                "Access-Control-Allow-Methods": "POST",
+                "Access-Control-Allow-Headers": "Authorization, Content-Type",
+                "Access-Control-Max-Age": "3600",
+            },
+        )
+
+    # Get data_source from query parameters instead of body
+    data_source = request.args.get("data_source")
+    if not data_source:
+        structured_logger(message="Missing data_source parameter", level="ERROR")
+        return https_fn.Response(
+            status=400,
+            response=json.dumps(
+                {
+                    "error": "Bad Request",
+                    "message": "Required data_source parameter is missing!",
+                }
+            ),
+            headers={
+                "Content-Type": "application/json",
+                "Access-Control-Allow-Origin": "*",
+            },
+        )
+
+    try:
+        # Log all headers for debugging
+        all_headers = dict(request.headers)
+        structured_logger(
+            message="Received headers",
+            headers=all_headers,
+            auth_header=request.headers.get(
+                "Authorization", "No Authorization header found"
+            ),
+        )
+
+        # Get the Authorization header
+        auth_header = request.headers.get("Authorization", "")
+        if not auth_header.startswith("Bearer "):
+            structured_logger(
+                message="Invalid Authorization header",
+                received_header=auth_header,
+                level="ERROR",
+            )
+            raise auth.InvalidIdTokenError("No valid authorization header found")
+
+        # Extract the token from the Authorization header
+        token = auth_header.split("Bearer ")[1]
+        if not token:
+            structured_logger(message="Empty Bearer token", level="ERROR")
+            raise auth.InvalidIdTokenError("Empty Bearer token")
+
+        # Verify the Firebase ID token and log its contents
+        decoded_token = auth.verify_id_token(token)
+        structured_logger(
+            message="Decoded token contents",
+            token_empty=decoded_token is None or decoded_token == "",
+        )
+
+        uid = decoded_token.get("uid") or decoded_token.get("sub")
+        if not uid:
+            raise auth.InvalidIdTokenError("No uid or sub claim found in token")
+
+        structured_logger(message="Token verified", uid=uid)
+
+        db = firestore.Client()
+        doc = db.collection("users").document(uid).get()
+
+        if not doc.exists:
+            structured_logger(message="User not found", uid=uid, level="ERROR")
+            return https_fn.Response(
+                status=404,
+                response=json.dumps(
+                    {
+                        "error": "Not Found",
+                        "message": f"User {uid} does not exist in Firestore",
+                    }
+                ),
+                headers={
+                    "Content-Type": "application/json",
+                    "Access-Control-Allow-Origin": "*",
+                },
+            )
+
+        stream_doc = (
+            db.collection("users")
+            .document(uid)
+            .collection("stream")
+            .document(data_source)
+            .get()
+        )
+        if not stream_doc.exists:
+            # handle error (e.g., return 404 or similar)
+            return https_fn.Response(
+                status=404,
+                response=json.dumps(
+                    {
+                        "error": "Not Found",
+                        "message": f"Stream data for user {uid} and data source {data_source} not found",
+                    }
+                ),
+                headers={
+                    "Content-Type": "application/json",
+                    "Access-Control-Allow-Origin": "*",
+                },
+            )
+
+        stream_data = stream_doc.to_dict()
+
+        if not stream_data or not stream_data.get("refreshToken"):
+            structured_logger(
+                message="No refresh token found",
+                uid=uid,
+                data_source=data_source,
+                level="ERROR",
+            )
+            return https_fn.Response(
+                status=400,
+                response=json.dumps(
+                    {
+                        "error": "Bad Request",
+                        "message": f"Bad Request - No refresh token found for user {uid} and data source {data_source}",
+                    }
+                ),
+                headers={
+                    "Content-Type": "application/json",
+                    "Access-Control-Allow-Origin": "*",
+                },
+            )
+
+        if data_source == "strava":
+            try:
+                strava_refresh_oauth_token(db, uid, stream_data["refreshToken"])
+                structured_logger(
+                    message="Token refresh successful",
+                    uid=uid,
+                    data_source=data_source,
+                )
+                return https_fn.Response(
+                    status=200,
+                    response=json.dumps(
+                        {"message": "Token refreshed successfully for Strava."}
+                    ),
+                    headers={
+                        "Content-Type": "application/json",
+                        "Access-Control-Allow-Origin": "*",
+                    },
+                )
+            except ValueError as e:
+                if "credentials not found" in str(e):
+                    structured_logger(
+                        message="Strava credentials not found",
+                        error=str(e),
+                        level="ERROR",
+                        traceback=traceback.format_exc(),
+                    )
+                    return https_fn.Response(
+                        status=500,
+                        response=json.dumps(
+                            {
+                                "error": "Internal Server Error",
+                                "message": "Internal Server Error - Strava credentials not found in Secret Manager",
+                            }
+                        ),
+                        headers={
+                            "Content-Type": "application/json",
+                            "Access-Control-Allow-Origin": "*",
+                        },
+                    )
+                raise
+        else:
+            structured_logger(
+                message="Unsupported data source",
+                data_source=data_source,
+                level="ERROR",
+            )
+            return https_fn.Response(
+                status=400,
+                response=json.dumps(
+                    {
+                        "error": "Bad Request",
+                        "message": f"Bad Request - Unsupported data source: {data_source}",
+                    }
+                ),
+                headers={
+                    "Content-Type": "application/json",
+                    "Access-Control-Allow-Origin": "*",
+                },
+            )
+
+    except auth.InvalidIdTokenError:
+        structured_logger(
+            message="Invalid Firebase ID Token; JWT Token Issue",
+            level="ERROR",
+            auth=auth_header,
+            traceback=traceback.format_exc(),
+        )
+        return https_fn.Response(
+            status=401,
+            response=json.dumps(
+                {
+                    "error": "Invalid Token",
+                    "message": "Invalid Firebase ID Token; JWT Token Issue",
+                }
+            ),
+            headers={
+                "Content-Type": "application/json",
+                "Access-Control-Allow-Origin": "*",
+            },
+        )
+    except auth.ExpiredIdTokenError:
+        structured_logger(
+            message="Expired token", level="ERROR", traceback=traceback.format_exc()
+        )
+        return https_fn.Response(
+            status=401,
+            response=json.dumps(
+                {"error": "Unauthorized", "message": "Unauthorized - Expired token"}
+            ),
+            headers={
+                "Content-Type": "application/json",
+                "Access-Control-Allow-Origin": "*",
+            },
+        )
+    except auth.RevokedIdTokenError:
+        structured_logger(
+            message="Revoked token", level="ERROR", traceback=traceback.format_exc()
+        )
+        return https_fn.Response(
+            status=401,
+            response=json.dumps(
+                {"error": "Unauthorized", "message": "Unauthorized - Revoked token"}
+            ),
+            headers={
+                "Content-Type": "application/json",
+                "Access-Control-Allow-Origin": "*",
+            },
+        )
+    except Exception as e:
+        structured_logger(
+            message="Error in token refresh",
+            error=str(e),
+            level="ERROR",
+            traceback=traceback.format_exc(),
+        )
+        return https_fn.Response(
+            status=500,
+            response=json.dumps({"error": "Internal Server Error", "message": str(e)}),
+            headers={
+                "Content-Type": "application/json",
+                "Access-Control-Allow-Origin": "*",
+            },
+        )
path: cloud_functions/token_refresh/main.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid catching all exceptions at the end of the function**

The final `except Exception as e:` block will swallow any unexpected errors, making root-cause analysis more difficult. Where possible, handle known exception types explicitly and allow truly unexpected errors to propagate or be caught by a global handler.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

300-300: Do not catch blind exception: `Exception`

(BLE001)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,314 +1,314 @@
-"""Main Entry point for Token Refresh."""
-import json
-import traceback
-
-from firebase_admin import auth, initialize_app
-from firebase_functions import https_fn, options
-from google.cloud import firestore
-
-from .shared.logger_utils import create_structured_logger
-from .streams.strava import strava_refresh_oauth_token
-
-structured_logger = create_structured_logger(__name__)
-
-try:
-    initialize_app()
-    structured_logger(message="Firebase Admin initialized successfully")
-except Exception as e:
-    structured_logger(
-        message="Error initializing Firebase Admin",
-        error=str(e),
-        level="ERROR",
-        traceback=traceback.format_exc(),
-    )
-    raise
-
-
-@https_fn.on_request(
-    cors=options.CorsOptions(cors_origins=["*"], cors_methods=["POST", "OPTIONS"])
-)
-def token_refresh(request: https_fn.Request) -> https_fn.Response:
-    """Cloud function taking http parameters to perform update of tokens.
-
-    Args:
-        request (http request): http request.
-
-    Note: At current time, it registers the parameters uid (firebase user id) and data_source.
-    """
-    # Log all request details at the start
-    structured_logger(
-        message="Request received",
-        method=request.method,
-        headers=dict(request.headers),
-        url=request.url,
-        args=dict(request.args),
-    )
-    try:
-        body = request.get_json(silent=True)
-        structured_logger(message="Request body", body=body)
-    except Exception as e:
-        structured_logger(
-            message="Error parsing request body",
-            error=str(e),
-            level="ERROR",
-            traceback=traceback.format_exc(),
-        )
-
-    # Handle OPTIONS request for CORS preflight
-    if request.method == "OPTIONS":
-        return https_fn.Response(
-            status=204,
-            headers={
-                "Access-Control-Allow-Origin": "*",
-                "Access-Control-Allow-Methods": "POST",
-                "Access-Control-Allow-Headers": "Authorization, Content-Type",
-                "Access-Control-Max-Age": "3600",
-            },
-        )
-
-    # Get data_source from query parameters instead of body
-    data_source = request.args.get("data_source")
-    if not data_source:
-        structured_logger(message="Missing data_source parameter", level="ERROR")
-        return https_fn.Response(
-            status=400,
-            response=json.dumps(
-                {
-                    "error": "Bad Request",
-                    "message": "Required data_source parameter is missing!",
-                }
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-
-    try:
-        # Log all headers for debugging
-        all_headers = dict(request.headers)
-        structured_logger(
-            message="Received headers",
-            headers=all_headers,
-            auth_header=request.headers.get(
-                "Authorization", "No Authorization header found"
-            ),
-        )
-
-        # Get the Authorization header
-        auth_header = request.headers.get("Authorization", "")
-        if not auth_header.startswith("Bearer "):
-            structured_logger(
-                message="Invalid Authorization header",
-                received_header=auth_header,
-                level="ERROR",
-            )
-            raise auth.InvalidIdTokenError("No valid authorization header found")
-
-        # Extract the token from the Authorization header
-        token = auth_header.split("Bearer ")[1]
-        if not token:
-            structured_logger(message="Empty Bearer token", level="ERROR")
-            raise auth.InvalidIdTokenError("Empty Bearer token")
-
-        # Verify the Firebase ID token and log its contents
-        decoded_token = auth.verify_id_token(token)
-        structured_logger(
-            message="Decoded token contents",
-            token_empty=decoded_token is None or decoded_token == "",
-        )
-
-        uid = decoded_token.get("uid") or decoded_token.get("sub")
-        if not uid:
-            raise auth.InvalidIdTokenError("No uid or sub claim found in token")
-
-        structured_logger(message="Token verified", uid=uid)
-
-        db = firestore.Client()
-        doc = db.collection("users").document(uid).get()
-
-        if not doc.exists:
-            structured_logger(message="User not found", uid=uid, level="ERROR")
-            return https_fn.Response(
-                status=404,
-                response=json.dumps(
-                    {
-                        "error": "Not Found",
-                        "message": f"User {uid} does not exist in Firestore",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        stream_doc = (
-            db.collection("users")
-            .document(uid)
-            .collection("stream")
-            .document(data_source)
-            .get()
-        )
-        if not stream_doc.exists:
-            # handle error (e.g., return 404 or similar)
-            return https_fn.Response(
-                status=404,
-                response=json.dumps(
-                    {
-                        "error": "Not Found",
-                        "message": f"Stream data for user {uid} and data source {data_source} not found",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        stream_data = stream_doc.to_dict()
-
-        if not stream_data or not stream_data.get("refreshToken"):
-            structured_logger(
-                message="No refresh token found",
-                uid=uid,
-                data_source=data_source,
-                level="ERROR",
-            )
-            return https_fn.Response(
-                status=400,
-                response=json.dumps(
-                    {
-                        "error": "Bad Request",
-                        "message": f"Bad Request - No refresh token found for user {uid} and data source {data_source}",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        if data_source == "strava":
-            try:
-                strava_refresh_oauth_token(db, uid, stream_data["refreshToken"])
-                structured_logger(
-                    message="Token refresh successful",
-                    uid=uid,
-                    data_source=data_source,
-                )
-                return https_fn.Response(
-                    status=200,
-                    response=json.dumps(
-                        {"message": "Token refreshed successfully for Strava."}
-                    ),
-                    headers={
-                        "Content-Type": "application/json",
-                        "Access-Control-Allow-Origin": "*",
-                    },
-                )
-            except ValueError as e:
-                if "credentials not found" in str(e):
-                    structured_logger(
-                        message="Strava credentials not found",
-                        error=str(e),
-                        level="ERROR",
-                        traceback=traceback.format_exc(),
-                    )
-                    return https_fn.Response(
-                        status=500,
-                        response=json.dumps(
-                            {
-                                "error": "Internal Server Error",
-                                "message": "Internal Server Error - Strava credentials not found in Secret Manager",
-                            }
-                        ),
-                        headers={
-                            "Content-Type": "application/json",
-                            "Access-Control-Allow-Origin": "*",
-                        },
-                    )
-                raise
-        else:
-            structured_logger(
-                message="Unsupported data source",
-                data_source=data_source,
-                level="ERROR",
-            )
-            return https_fn.Response(
-                status=400,
-                response=json.dumps(
-                    {
-                        "error": "Bad Request",
-                        "message": f"Bad Request - Unsupported data source: {data_source}",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-    except auth.InvalidIdTokenError:
-        structured_logger(
-            message="Invalid Firebase ID Token; JWT Token Issue",
-            level="ERROR",
-            auth=auth_header,
-            traceback=traceback.format_exc(),
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {
-                    "error": "Invalid Token",
-                    "message": "Invalid Firebase ID Token; JWT Token Issue",
-                }
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except auth.ExpiredIdTokenError:
-        structured_logger(
-            message="Expired token", level="ERROR", traceback=traceback.format_exc()
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {"error": "Unauthorized", "message": "Unauthorized - Expired token"}
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except auth.RevokedIdTokenError:
-        structured_logger(
-            message="Revoked token", level="ERROR", traceback=traceback.format_exc()
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {"error": "Unauthorized", "message": "Unauthorized - Revoked token"}
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except Exception as e:
-        structured_logger(
-            message="Error in token refresh",
-            error=str(e),
-            level="ERROR",
-            traceback=traceback.format_exc(),
-        )
-        return https_fn.Response(
-            status=500,
-            response=json.dumps({"error": "Internal Server Error", "message": str(e)}),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
+"""Main Entry point for Token Refresh."""
+import json
+import traceback
+
+from firebase_admin import auth, initialize_app
+from firebase_functions import https_fn, options
+from google.cloud import firestore
+
+from .shared.logger_utils import create_structured_logger
+from .streams.strava import strava_refresh_oauth_token
+
+structured_logger = create_structured_logger(__name__)
+
+try:
+    initialize_app()
+    structured_logger(message="Firebase Admin initialized successfully")
+except Exception as e:
+    structured_logger(
+        message="Error initializing Firebase Admin",
+        error=str(e),
+        level="ERROR",
+        traceback=traceback.format_exc(),
+    )
+    raise
+
+
+@https_fn.on_request(
+    cors=options.CorsOptions(cors_origins=["*"], cors_methods=["POST", "OPTIONS"])
+)
+def token_refresh(request: https_fn.Request) -> https_fn.Response:
+    """Cloud function taking http parameters to perform update of tokens.
+
+    Args:
+        request (http request): http request.
+
+    Note: At current time, it registers the parameters uid (firebase user id) and data_source.
+    """
+    # Log all request details at the start
+    structured_logger(
+        message="Request received",
+        method=request.method,
+        headers=dict(request.headers),
+        url=request.url,
+        args=dict(request.args),
+    )
+    try:
+        body = request.get_json(silent=True)
+        structured_logger(message="Request body", body=body)
+    except Exception as e:
+        structured_logger(
+            message="Error parsing request body",
+            error=str(e),
+            level="ERROR",
+            traceback=traceback.format_exc(),
+        )
+
path: cloud_functions/token_refresh/main.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Catch specific exceptions for JSON parsing**

Catching a broad `Exception` around `request.get_json` can mask unexpected errors. Instead, catch the relevant parsing exception (`BadRequest` from `firebase_functions.https_fn` or `ValueError`), and define a default `body` in the `except` block to avoid potential `UnboundLocalError`.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

49-49: Do not catch blind exception: `Exception`

(BLE001)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,314 +1,314 @@
-"""Main Entry point for Token Refresh."""
-import json
-import traceback
-
-from firebase_admin import auth, initialize_app
-from firebase_functions import https_fn, options
-from google.cloud import firestore
-
-from .shared.logger_utils import create_structured_logger
-from .streams.strava import strava_refresh_oauth_token
-
-structured_logger = create_structured_logger(__name__)
-
-try:
-    initialize_app()
-    structured_logger(message="Firebase Admin initialized successfully")
-except Exception as e:
-    structured_logger(
-        message="Error initializing Firebase Admin",
-        error=str(e),
-        level="ERROR",
-        traceback=traceback.format_exc(),
-    )
-    raise
-
-
-@https_fn.on_request(
-    cors=options.CorsOptions(cors_origins=["*"], cors_methods=["POST", "OPTIONS"])
-)
-def token_refresh(request: https_fn.Request) -> https_fn.Response:
-    """Cloud function taking http parameters to perform update of tokens.
-
-    Args:
-        request (http request): http request.
-
-    Note: At current time, it registers the parameters uid (firebase user id) and data_source.
-    """
-    # Log all request details at the start
-    structured_logger(
-        message="Request received",
-        method=request.method,
-        headers=dict(request.headers),
-        url=request.url,
-        args=dict(request.args),
-    )
-    try:
-        body = request.get_json(silent=True)
-        structured_logger(message="Request body", body=body)
-    except Exception as e:
-        structured_logger(
-            message="Error parsing request body",
-            error=str(e),
-            level="ERROR",
-            traceback=traceback.format_exc(),
-        )
-
-    # Handle OPTIONS request for CORS preflight
-    if request.method == "OPTIONS":
-        return https_fn.Response(
-            status=204,
-            headers={
-                "Access-Control-Allow-Origin": "*",
-                "Access-Control-Allow-Methods": "POST",
-                "Access-Control-Allow-Headers": "Authorization, Content-Type",
-                "Access-Control-Max-Age": "3600",
-            },
-        )
-
-    # Get data_source from query parameters instead of body
-    data_source = request.args.get("data_source")
-    if not data_source:
-        structured_logger(message="Missing data_source parameter", level="ERROR")
-        return https_fn.Response(
-            status=400,
-            response=json.dumps(
-                {
-                    "error": "Bad Request",
-                    "message": "Required data_source parameter is missing!",
-                }
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-
-    try:
-        # Log all headers for debugging
-        all_headers = dict(request.headers)
-        structured_logger(
-            message="Received headers",
-            headers=all_headers,
-            auth_header=request.headers.get(
-                "Authorization", "No Authorization header found"
-            ),
-        )
-
-        # Get the Authorization header
-        auth_header = request.headers.get("Authorization", "")
-        if not auth_header.startswith("Bearer "):
-            structured_logger(
-                message="Invalid Authorization header",
-                received_header=auth_header,
-                level="ERROR",
-            )
-            raise auth.InvalidIdTokenError("No valid authorization header found")
-
-        # Extract the token from the Authorization header
-        token = auth_header.split("Bearer ")[1]
-        if not token:
-            structured_logger(message="Empty Bearer token", level="ERROR")
-            raise auth.InvalidIdTokenError("Empty Bearer token")
-
-        # Verify the Firebase ID token and log its contents
-        decoded_token = auth.verify_id_token(token)
-        structured_logger(
-            message="Decoded token contents",
-            token_empty=decoded_token is None or decoded_token == "",
-        )
-
-        uid = decoded_token.get("uid") or decoded_token.get("sub")
-        if not uid:
-            raise auth.InvalidIdTokenError("No uid or sub claim found in token")
-
-        structured_logger(message="Token verified", uid=uid)
-
-        db = firestore.Client()
-        doc = db.collection("users").document(uid).get()
-
-        if not doc.exists:
-            structured_logger(message="User not found", uid=uid, level="ERROR")
-            return https_fn.Response(
-                status=404,
-                response=json.dumps(
-                    {
-                        "error": "Not Found",
-                        "message": f"User {uid} does not exist in Firestore",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        stream_doc = (
-            db.collection("users")
-            .document(uid)
-            .collection("stream")
-            .document(data_source)
-            .get()
-        )
-        if not stream_doc.exists:
-            # handle error (e.g., return 404 or similar)
-            return https_fn.Response(
-                status=404,
-                response=json.dumps(
-                    {
-                        "error": "Not Found",
-                        "message": f"Stream data for user {uid} and data source {data_source} not found",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        stream_data = stream_doc.to_dict()
-
-        if not stream_data or not stream_data.get("refreshToken"):
-            structured_logger(
-                message="No refresh token found",
-                uid=uid,
-                data_source=data_source,
-                level="ERROR",
-            )
-            return https_fn.Response(
-                status=400,
-                response=json.dumps(
-                    {
-                        "error": "Bad Request",
-                        "message": f"Bad Request - No refresh token found for user {uid} and data source {data_source}",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-        if data_source == "strava":
-            try:
-                strava_refresh_oauth_token(db, uid, stream_data["refreshToken"])
-                structured_logger(
-                    message="Token refresh successful",
-                    uid=uid,
-                    data_source=data_source,
-                )
-                return https_fn.Response(
-                    status=200,
-                    response=json.dumps(
-                        {"message": "Token refreshed successfully for Strava."}
-                    ),
-                    headers={
-                        "Content-Type": "application/json",
-                        "Access-Control-Allow-Origin": "*",
-                    },
-                )
-            except ValueError as e:
-                if "credentials not found" in str(e):
-                    structured_logger(
-                        message="Strava credentials not found",
-                        error=str(e),
-                        level="ERROR",
-                        traceback=traceback.format_exc(),
-                    )
-                    return https_fn.Response(
-                        status=500,
-                        response=json.dumps(
-                            {
-                                "error": "Internal Server Error",
-                                "message": "Internal Server Error - Strava credentials not found in Secret Manager",
-                            }
-                        ),
-                        headers={
-                            "Content-Type": "application/json",
-                            "Access-Control-Allow-Origin": "*",
-                        },
-                    )
-                raise
-        else:
-            structured_logger(
-                message="Unsupported data source",
-                data_source=data_source,
-                level="ERROR",
-            )
-            return https_fn.Response(
-                status=400,
-                response=json.dumps(
-                    {
-                        "error": "Bad Request",
-                        "message": f"Bad Request - Unsupported data source: {data_source}",
-                    }
-                ),
-                headers={
-                    "Content-Type": "application/json",
-                    "Access-Control-Allow-Origin": "*",
-                },
-            )
-
-    except auth.InvalidIdTokenError:
-        structured_logger(
-            message="Invalid Firebase ID Token; JWT Token Issue",
-            level="ERROR",
-            auth=auth_header,
-            traceback=traceback.format_exc(),
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {
-                    "error": "Invalid Token",
-                    "message": "Invalid Firebase ID Token; JWT Token Issue",
-                }
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except auth.ExpiredIdTokenError:
-        structured_logger(
-            message="Expired token", level="ERROR", traceback=traceback.format_exc()
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {"error": "Unauthorized", "message": "Unauthorized - Expired token"}
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except auth.RevokedIdTokenError:
-        structured_logger(
-            message="Revoked token", level="ERROR", traceback=traceback.format_exc()
-        )
-        return https_fn.Response(
-            status=401,
-            response=json.dumps(
-                {"error": "Unauthorized", "message": "Unauthorized - Revoked token"}
-            ),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
-    except Exception as e:
-        structured_logger(
-            message="Error in token refresh",
-            error=str(e),
-            level="ERROR",
-            traceback=traceback.format_exc(),
-        )
-        return https_fn.Response(
-            status=500,
-            response=json.dumps({"error": "Internal Server Error", "message": str(e)}),
-            headers={
-                "Content-Type": "application/json",
-                "Access-Control-Allow-Origin": "*",
-            },
-        )
+"""Main Entry point for Token Refresh."""
+import json
+import traceback
+
+from firebase_admin import auth, initialize_app
+from firebase_functions import https_fn, options
+from google.cloud import firestore
+
+from .shared.logger_utils import create_structured_logger
+from .streams.strava import strava_refresh_oauth_token
+
+structured_logger = create_structured_logger(__name__)
+
+try:
+    initialize_app()
+    structured_logger(message="Firebase Admin initialized successfully")
+except Exception as e:
+    structured_logger(
+        message="Error initializing Firebase Admin",
+        error=str(e),
+        level="ERROR",
+        traceback=traceback.format_exc(),
+    )
+    raise
+
+
+@https_fn.on_request(
+    cors=options.CorsOptions(cors_origins=["*"], cors_methods=["POST", "OPTIONS"])
+)
+def token_refresh(request: https_fn.Request) -> https_fn.Response:
+    """Cloud function taking http parameters to perform update of tokens.
+
+    Args:
+        request (http request): http request.
+
+    Note: At current time, it registers the parameters uid (firebase user id) and data_source.
+    """
+    # Log all request details at the start
+    structured_logger(
+        message="Request received",
+        method=request.method,
+        headers=dict(request.headers),
+        url=request.url,
+        args=dict(request.args),
+    )
path: cloud_functions/token_refresh/main.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Avoid logging sensitive headers like authorization tokens**

The structured logger records all request headers (including `Authorization`) which may leak bearer tokens. Filter out or redact sensitive headers before logging:

```python
safe_headers = {k: v for k, v in request.headers.items() if k.lower() != "authorization"}
structured_logger(
    message="Request received",
    method=request.method,
    headers=safe_headers,
    url=request.url,
    args=dict(request.args),
)
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Install pre-commit
+              run: pip install pre-commit
+
+            - name: Run pre-commit checks
+              run: pre-commit run --all-files
+
+            - name: Copy shared code into each function directory
               run: |
-                  pip install pre-commit
-                  pre-commit install
-                  # Run once to populate the cache
-                  pre-commit run --all-files || true
-
-            - name: Save pre-commit cache
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
-              uses: actions/cache/save@v4
+                  for fn in cloud_functions/*; do
+                    if [ -d "$fn" ] && [ "$(basename $fn)" != "shared" ]; then
+                      rm -rf "$fn/shared"
+                      mkdir -p "$fn/shared"
+                      cp -r cloud_functions/shared/. "$fn/shared/"
+                    fi
+                  done
+
+            - name: Run tests
+              run: PYTHONPATH=$PWD poetry run pytest tests/
+
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Enhance test reporting with coverage thresholds**

You may want to add `--cov-report=xml` or a `--cov-fail-under=<threshold>` flag to `pytest` so that coverage results are published and enforced in CI.

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commits b31e82a to 748bfd1

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Broaden pre-commit cache restore keys**

The `restore-keys: ${{ runner.os }}-pre-commit-` entry may miss previous caches if the hash changes. Adding a fallback like `-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}` can improve cache hit rates.

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commits b31e82a to 748bfd1

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:51:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Install pre-commit
+              run: pip install pre-commit
+
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Pin `pre-commit` installation to ensure repeatability**

Installing `pre-commit` without a version constraint may introduce unexpected changes. Consider pinning to the version in your `pyproject.toml`:

```yaml
run: pip install pre-commit==${{ hashFiles('pyproject.toml') }}
```

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-09T01:51:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Install pre-commit
+              run: pip install pre-commit
+
+            - name: Run pre-commit checks
+              run: pre-commit run --all-files
+
+            - name: Copy shared code into each function directory
               run: |
-                  pip install pre-commit
-                  pre-commit install
-                  # Run once to populate the cache
-                  pre-commit run --all-files || true
-
-            - name: Save pre-commit cache
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
-              uses: actions/cache/save@v4
+                  for fn in cloud_functions/*; do
+                    if [ -d "$fn" ] && [ "$(basename $fn)" != "shared" ]; then
+                      rm -rf "$fn/shared"
+                      mkdir -p "$fn/shared"
+                      cp -r cloud_functions/shared/. "$fn/shared/"
+                    fi
+                  done
+
+            - name: Run tests
+              run: PYTHONPATH=$PWD poetry run pytest tests/
+
path: .github/workflows/dev-deploy.yaml
user: login: santoshgdev
body: why xml? what do you mean so that coverage results are published and enforced in CI?
created_at: 2025-05-09T01:52:37Z
author_association: OWNER
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Install pre-commit
+              run: pip install pre-commit
+
+            - name: Run pre-commit checks
+              run: pre-commit run --all-files
+
+            - name: Copy shared code into each function directory
               run: |
-                  pip install pre-commit
-                  pre-commit install
-                  # Run once to populate the cache
-                  pre-commit run --all-files || true
-
-            - name: Save pre-commit cache
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
-              uses: actions/cache/save@v4
+                  for fn in cloud_functions/*; do
+                    if [ -d "$fn" ] && [ "$(basename $fn)" != "shared" ]; then
+                      rm -rf "$fn/shared"
+                      mkdir -p "$fn/shared"
+                      cp -r cloud_functions/shared/. "$fn/shared/"
+                    fi
+                  done
+
+            - name: Run tests
+              run: PYTHONPATH=$PWD poetry run pytest tests/
+
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: @santoshgdev Great questions! Let me clarify:

1. **Why XML?** 
   - The `--cov-report=xml` flag generates a coverage report in Cobertura XML format, which is widely supported by CI/CD tools and services like Codecov, SonarQube, or GitHub's own code scanning features.
   - This standardized format allows you to visualize coverage directly in your GitHub PRs or on dashboards.

2. **Publishing and enforcing in CI means:**
   - **Publishing**: Making the coverage data available for tracking over time. You can upload XML reports as artifacts or send them to coverage tracking services.
   - **Enforcing**: Using `--cov-fail-under=<threshold>` makes your CI pipeline fail if coverage falls below your defined threshold (e.g., 80%), acting as a quality gate.

Here's a practical example you could add to your workflow:

```yaml
- name: Run tests with coverage
  run: |
    PYTHONPATH=$PWD poetry run pytest tests/ \
      --cov=cloud_functions \
      --cov-report=xml \
      --cov-report=term \
      --cov-fail-under=80
```

This would require 80% code coverage and generate both console output and an XML report for tracking.

Would you like me to suggest how to integrate this with a coverage visualization service?

(\_/）
(•ㅅ•）
/ > 📊


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@santoshgdev Great questions! Let me clarify:

1. **Why XML?** 
   - The `--cov-report=xml` flag generates a coverage report in Cobertura XML format, which is widely supported by CI/CD tools and services like Codecov, SonarQube, or GitHub's own code scanning features.
   - This standardized format allows you to visualize coverage directly in your GitHub PRs or on dashboards.

2. **Publishing and enforcing in CI means:**
   - **Publishing**: Making the coverage data available for tracking over time. You can upload XML reports as artifacts or send them to coverage tracking services.
   - **Enforcing**: Using `--cov-fail-under=<threshold>` makes your CI pipeline fail if coverage falls below your defined threshold (e.g., 80%), acting as a quality gate.

Here's a practical example you could add to your workflow:

```yaml
- name: Run tests with coverage
  run: |
    PYTHONPATH=$PWD poetry run pytest tests/ \
      --cov=cloud_functions \
      --cov-report=xml \
      --cov-report=term \
      --cov-fail-under=80
```

This would require 80% code coverage and generate both console output and an XML report for tracking.

Would you like me to suggest how to integrate this with a coverage visualization service?

(\_/）
(•ㅅ•）
/ > 📊</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-09T01:53:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,117 +1,118 @@
-import json
-from unittest.mock import patch
-
-import flask
-from firebase_admin import auth
-
-from cloud_functions.token_refresh.main import token_refresh
-from tests.cloud_functions.testing_utils import (
-    populate_inmemory_firestore_with_users_and_streams,
-)
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                response = token_refresh(mock_request)
-                assert response.status_code == 200
-                assert (
-                    "Token refreshed successfully for Strava"
-                    in json.loads(response.response[0])["message"]
-                )
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_missing_data_source(
-    mock_verify, mock_request, mock_decoded_token
-):
-    """Test token refresh with missing data source."""
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                mock_request.args = {}
-                response = token_refresh(mock_request)
-                assert response.status_code == 400
-                assert (
-                    "Required data_source parameter is missing"
-                    in json.loads(response.response[0])["message"]
-                )
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_invalid_token(mock_verify, mock_request, mock_decoded_token):
-    """Test token refresh with invalid token."""
-
-    # Setup in-memory Firestore with one user and strava stream
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                mock_verify.side_effect = auth.InvalidIdTokenError("Invalid token")
-                response = token_refresh(mock_request)
-                assert response.status_code == 401
-                assert (
-                    "Invalid Firebase ID Token; JWT Token Issu"
-                    in json.loads(response.response[0])["message"]
-                )
+import json
+from unittest.mock import patch
+
+import flask
+from firebase_admin import auth
+
+from cloud_functions.token_refresh.main import token_refresh
+from tests.cloud_functions.testing_utils import (
+    populate_inmemory_firestore_with_users_and_streams,
+)
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                response = token_refresh(mock_request)
+                assert response.status_code == 200
+                assert (
+                    "Token refreshed successfully for Strava"
+                    in json.loads(response.response[0])["message"]
+                )
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_missing_data_source(
+    mock_verify, mock_request, mock_decoded_token
+):
+    """Test token refresh with missing data source."""
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                mock_request.args = {}
+                response = token_refresh(mock_request)
+                assert response.status_code == 400
+                assert (
+                    "Required data_source parameter is missing"
+                    in json.loads(response.response[0])["message"]
+                )
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_invalid_token(mock_verify, mock_request, mock_decoded_token):
+    """Test token refresh with invalid token."""
+
+    # Setup in-memory Firestore with one user and strava stream
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                mock_verify.side_effect = auth.InvalidIdTokenError("Invalid token")
+                response = token_refresh(mock_request)
+                assert response.status_code == 401
+                assert (
+                assert (
+                    "Invalid Firebase ID Token; JWT Token Issue"
+                    in json.loads(response.response[0])["message"]
+                )
path: tests/cloud_functions/token_refresh/test_main.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix duplicate assert statement.**

There's a syntax error with a duplicated `assert (` statement that needs to be fixed.

```diff
                assert response.status_code == 401
-                assert (
                assert (
                    "Invalid Firebase ID Token; JWT Token Issue"
                    in json.loads(response.response[0])["message"]
                )
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                assert response.status_code == 401
                assert (
                    "Invalid Firebase ID Token; JWT Token Issue"
                    in json.loads(response.response[0])["message"]
                )
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

115-115: SyntaxError: Expected an identifier, but found a keyword 'assert' that cannot be used here

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:53:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,41 +1,40 @@
-[project]
-name = "fitnessllm_dataplatform"
-version = "0.1.0"
-description = ""
-authors = [
-    { name = "Santosh G", email = "santosh@santoshg.io" }
-]
-
-[tool.poetry.dependencies]
-python = "3.12.2"
-cloudpathlib = {extras = ["gs"], version = "^0.20.0"}
-typing-extensions = "^4.12.2"
-fire = "^0.7.0"
-google-cloud-secret-manager = "^2.22.1"
-tqdm = "^4.67.1"
-numpy = "^2.2.2"
-pandas = "^2.2.3"
-google-cloud-bigquery = {extras = ["pandas"], version = "^3.29.0"}
-joblib = "^1.4.2"
-tqdm-joblib = "^0.0.4"
-google-cloud-bigquery-storage = "^2.27.0"
-stravalib = "^2.1"
-pytest = "^8.3.4"
-pre-commit = "^4.1.0"
-firebase-admin = "^6.7.0"
-redis = "^5.2.1"
-jinja2 = "^3.1.5"
-sqlglot = "^26.7.0"
-
-[tool.poetry.dev-dependencies]
-beartype = "^0.19.0"
-pytest-cov = "^6.0.0"
-firebase-functions = "^0.4.2"
-faker = "^37.1.0"
-
-[build-system]
-requires = ["poetry-core>=1.0.0"]
-build-backend = "poetry.core.masonry.api"
-
-[tool.poetry.requires-plugins]
-poetry-plugin-export = ">=1.8"
+[project]
+name = "fitnessllm_dataplatform"
+version = "0.1.0"
+description = ""
+authors = [
+    { name = "Santosh G", email = "santosh@santoshg.io" }
+]
+
+[tool.poetry.dependencies]
+python = "^3.12.2"
+cloudpathlib = {extras = ["gs"], version = "^0.20.0"}
+typing-extensions = "^4.12.2"
+fire = "^0.7.0"
+google-cloud-secret-manager = "^2.22.1"
+tqdm = "^4.67.1"
+numpy = "^1.26.0"
+pandas = "^2.2.3"
+google-cloud-bigquery = {extras = ["pandas"], version = "^3.29.0"}
+joblib = "^1.4.2"
+tqdm-joblib = "^0.0.4"
+google-cloud-bigquery-storage = "^2.27.0"
+stravalib = "^2.1"
+pytest = "^8.3.4"
+pre-commit = "^4.1.0"
+firebase-admin = "^6.7.0"
+redis = "^5.2.1"
+jinja2 = "^3.1.5"
+sqlglot = "^26.7.0"
+faker = "^37.1.0"
+flask = "^3.1.0"
+firebase-functions = "^0.4.2"
+beartype = "^0.19.0"
+pytest-cov = "^6.0.0"
path: pyproject.toml
user: login: santoshgdev
body: this just caused issues keeping it in dev. don't suggest this again.
created_at: 2025-05-09T01:54:23Z
author_association: OWNER
diff_hunk: @@ -1,41 +1,40 @@
-[project]
-name = "fitnessllm_dataplatform"
-version = "0.1.0"
-description = ""
-authors = [
-    { name = "Santosh G", email = "santosh@santoshg.io" }
-]
-
-[tool.poetry.dependencies]
-python = "3.12.2"
-cloudpathlib = {extras = ["gs"], version = "^0.20.0"}
-typing-extensions = "^4.12.2"
-fire = "^0.7.0"
-google-cloud-secret-manager = "^2.22.1"
-tqdm = "^4.67.1"
-numpy = "^2.2.2"
-pandas = "^2.2.3"
-google-cloud-bigquery = {extras = ["pandas"], version = "^3.29.0"}
-joblib = "^1.4.2"
-tqdm-joblib = "^0.0.4"
-google-cloud-bigquery-storage = "^2.27.0"
-stravalib = "^2.1"
-pytest = "^8.3.4"
-pre-commit = "^4.1.0"
-firebase-admin = "^6.7.0"
-redis = "^5.2.1"
-jinja2 = "^3.1.5"
-sqlglot = "^26.7.0"
-
-[tool.poetry.dev-dependencies]
-beartype = "^0.19.0"
-pytest-cov = "^6.0.0"
-firebase-functions = "^0.4.2"
-faker = "^37.1.0"
-
-[build-system]
-requires = ["poetry-core>=1.0.0"]
-build-backend = "poetry.core.masonry.api"
-
-[tool.poetry.requires-plugins]
-poetry-plugin-export = ">=1.8"
+[project]
+name = "fitnessllm_dataplatform"
+version = "0.1.0"
+description = ""
+authors = [
+    { name = "Santosh G", email = "santosh@santoshg.io" }
+]
+
+[tool.poetry.dependencies]
+python = "^3.12.2"
+cloudpathlib = {extras = ["gs"], version = "^0.20.0"}
+typing-extensions = "^4.12.2"
+fire = "^0.7.0"
+google-cloud-secret-manager = "^2.22.1"
+tqdm = "^4.67.1"
+numpy = "^1.26.0"
+pandas = "^2.2.3"
+google-cloud-bigquery = {extras = ["pandas"], version = "^3.29.0"}
+joblib = "^1.4.2"
+tqdm-joblib = "^0.0.4"
+google-cloud-bigquery-storage = "^2.27.0"
+stravalib = "^2.1"
+pytest = "^8.3.4"
+pre-commit = "^4.1.0"
+firebase-admin = "^6.7.0"
+redis = "^5.2.1"
+jinja2 = "^3.1.5"
+sqlglot = "^26.7.0"
+faker = "^37.1.0"
+flask = "^3.1.0"
+firebase-functions = "^0.4.2"
+beartype = "^0.19.0"
+pytest-cov = "^6.0.0"
path: pyproject.toml
user: login: coderabbitai[bot]
body: @santoshgdev I understand. I'll note this for future reviews - keeping the packages in the main dependencies section makes sense if separating them was causing issues in your CI/CD setup. Thanks for the clarification.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: santoshgdev
PR: santoshgdev/fitnessllm-dataplatform#24
File: pyproject.toml:29-33
Timestamp: 2025-05-09T01:54:49.716Z
Learning: For the fitnessllm-dataplatform repository, do not suggest separating dev dependencies from main dependencies in pyproject.toml as this caused issues for the project's CI/CD pipeline. All dependencies should remain in [tool.poetry.dependencies] section.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@santoshgdev I understand. I'll note this for future reviews - keeping the packages in the main dependencies section makes sense if separating them was causing issues in your CI/CD setup. Thanks for the clarification.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: santoshgdev
PR: santoshgdev/fitnessllm-dataplatform#24
File: pyproject.toml:29-33
Timestamp: 2025-05-09T01:54:49.716Z
Learning: For the fitnessllm-dataplatform repository, do not suggest separating dev dependencies from main dependencies in pyproject.toml as this caused issues for the project's CI/CD pipeline. All dependencies should remain in [tool.poetry.dependencies] section.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-09T01:54:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
path: .github/workflows/dev-deploy.yaml
user: login: santoshgdev
body: but shouldn't it only restore the particular cache that is relevant to the particular .pre-commit-config.yaml?
created_at: 2025-05-09T01:55:55Z
author_association: OWNER
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Install pre-commit
+              run: pip install pre-commit
+
+            - name: Run pre-commit checks
+              run: pre-commit run --all-files
+
+            - name: Copy shared code into each function directory
               run: |
-                  pip install pre-commit
-                  pre-commit install
-                  # Run once to populate the cache
-                  pre-commit run --all-files || true
-
-            - name: Save pre-commit cache
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
-              uses: actions/cache/save@v4
+                  for fn in cloud_functions/*; do
+                    if [ -d "$fn" ] && [ "$(basename $fn)" != "shared" ]; then
+                      rm -rf "$fn/shared"
+                      mkdir -p "$fn/shared"
+                      cp -r cloud_functions/shared/. "$fn/shared/"
+                    fi
+                  done
+
+            - name: Run tests
+              run: PYTHONPATH=$PWD poetry run pytest tests/
+
path: .github/workflows/dev-deploy.yaml
user: login: santoshgdev
body: yes please do
created_at: 2025-05-09T01:56:08Z
author_association: OWNER
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: @santoshgdev You're absolutely right! The cache should ideally be specific to the particular `.pre-commit-config.yaml` configuration for correctness.

What you've implemented with the simplified `${{ runner.os }}-pre-commit-` as the restore-key has a trade-off:

- **Pro**: Better cache hit rate (will fall back to any pre-commit cache if exact match not found)
- **Con**: Could restore a cache from a different pre-commit configuration

If you want to balance both correctness and performance, you could use a more specific restore-key like:
```yaml
restore-keys: |
  ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-
```

This would only restore caches matching your current pre-commit config (even if the poetry.lock changed), which is more correct than falling back to any pre-commit cache.

But your current approach is also valid if you're optimizing for build speed and aren't concerned about potentially restoring less-relevant caches.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: santoshgdev
PR: santoshgdev/fitnessllm-dataplatform#24
File: .github/workflows/dev-deploy.yaml:70-70
Timestamp: 2025-05-09T01:56:23.068Z
Learning: For GitHub Actions cache, using a more specific restore-key like `${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-` ensures that only caches matching the current pre-commit configuration are restored, which is important for correctness. Using a more general restore-key like `${{ runner.os }}-pre-commit-` optimizes for build speed but may restore less-relevant caches.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@santoshgdev You're absolutely right! The cache should ideally be specific to the particular `.pre-commit-config.yaml` configuration for correctness.

What you've implemented with the simplified `${{ runner.os }}-pre-commit-` as the restore-key has a trade-off:

- **Pro**: Better cache hit rate (will fall back to any pre-commit cache if exact match not found)
- **Con**: Could restore a cache from a different pre-commit configuration

If you want to balance both correctness and performance, you could use a more specific restore-key like:
```yaml
restore-keys: |
  ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-
```

This would only restore caches matching your current pre-commit config (even if the poetry.lock changed), which is more correct than falling back to any pre-commit cache.

But your current approach is also valid if you're optimizing for build speed and aren't concerned about potentially restoring less-relevant caches.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: santoshgdev
PR: santoshgdev/fitnessllm-dataplatform#24
File: .github/workflows/dev-deploy.yaml:70-70
Timestamp: 2025-05-09T01:56:23.068Z
Learning: For GitHub Actions cache, using a more specific restore-key like `${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-` ensures that only caches matching the current pre-commit configuration are restored, which is important for correctness. Using a more general restore-key like `${{ runner.os }}-pre-commit-` optimizes for build speed but may restore less-relevant caches.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-09T01:56:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Install pre-commit
+              run: pip install pre-commit
+
+            - name: Run pre-commit checks
+              run: pre-commit run --all-files
+
+            - name: Copy shared code into each function directory
               run: |
-                  pip install pre-commit
-                  pre-commit install
-                  # Run once to populate the cache
-                  pre-commit run --all-files || true
-
-            - name: Save pre-commit cache
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
-              uses: actions/cache/save@v4
+                  for fn in cloud_functions/*; do
+                    if [ -d "$fn" ] && [ "$(basename $fn)" != "shared" ]; then
+                      rm -rf "$fn/shared"
+                      mkdir -p "$fn/shared"
+                      cp -r cloud_functions/shared/. "$fn/shared/"
+                    fi
+                  done
+
+            - name: Run tests
+              run: PYTHONPATH=$PWD poetry run pytest tests/
+
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@santoshgdev I'd be happy to explain how to integrate code coverage visualization with your CI pipeline. Here's how to do it with Codecov, which is one of the most popular services:




First, let's set up coverage visualization using Codecov:

1. **Install pytest-cov** (if not already installed):
   ```bash
   poetry add --dev pytest-cov
   ```

2. **Update your test step** in `.github/workflows/dev-deploy.yaml`:
   ```yaml
   - name: Run tests with coverage
     run: |
       PYTHONPATH=$PWD poetry run pytest tests/ \
         --cov=cloud_functions \
         --cov-report=xml \
         --cov-report=term \
         --cov-fail-under=80
   ```

3. **Add the Codecov upload step** after your test step:
   ```yaml
   - name: Upload coverage to Codecov
     uses: codecov/codecov-action@v3
     with:
       token: ${{ secrets.CODECOV_TOKEN }} # Optional for public repos
       files: ./coverage.xml
       fail_ci_if_error: true
   ```

4. **Create a Codecov account** at https://codecov.io/ and connect it to your GitHub repository

5. **Create a codecov.yml** file in your repository root (optional but recommended):
   ```yaml
   coverage:
     status:
       project:
         default:
           target: 80%  # Adjust this percentage to your desired coverage threshold
           threshold: 1%  # Allow a 1% drop from the previous coverage
       patch:
         default:
           target: 80%
   ```

This setup will give you beautiful coverage charts on each PR, historical trends, and automatic comments showing coverage changes.

Alternative options:
- **Coveralls**: Similar to Codecov with a slightly different UI
- **GitHub Action artifacts**: Store the coverage report as an artifact without a third-party service

Would you like more specific details on any of these steps?

---

🏁 Script executed:
```
#!/bin/bash
# Check if pytest-cov is already in dependencies
grep -E "pytest-cov" pyproject.toml
```

Length of output: 57

---



</details>

@santoshgdev Great! I see you already have `pytest-cov = "^6.0.0"` in your dependencies, so we can skip the installation step. Here's how to integrate with Codecov:

1. **Update your test step** in `.github/workflows/dev-deploy.yaml` to generate coverage reports:
   ```yaml
   - name: Run tests with coverage
     run: |
       PYTHONPATH=$PWD poetry run pytest tests/ \
         --cov=cloud_functions \
         --cov-report=xml \
         --cov-report=term \
         --cov-fail-under=80
   ```

2. **Add the Codecov upload step** after your test step:
   ```yaml
   - name: Upload coverage to Codecov
     uses: codecov/codecov-action@v3
     with:
       files: ./coverage.xml
       fail_ci_if_error: false  # Set to true once integration is stable
   ```

3. **Sign up for Codecov**:
   - Go to https://codecov.io/
   - Sign in with your GitHub account
   - Authorize Codecov to access your repository
   - No token is required for public repositories

4. **Create a codecov.yml** file in your repository root for configuration:
   ```yaml
   coverage:
     status:
       project:
         default:
           target: 80%  # Your desired overall coverage target
           threshold: 1%  # Allow a 1% drop from the previous coverage
       patch:
         default:
           target: 80%  # Coverage required for new code
   comment:
     layout: "reach, diff, flags, files"
     behavior: default
     require_changes: false
   ```

After you push these changes, Codecov will automatically comment on your PRs with coverage reports and display trends over time. You'll get visual feedback on coverage changes directly in your GitHub workflow.

Would you like me to explain any part of this in more detail?


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

@santoshgdev I'd be happy to explain how to integrate code coverage visualization with your CI pipeline. Here's how to do it with Codecov, which is one of the most popular services:




First, let's set up coverage visualization using Codecov:

1. **Install pytest-cov** (if not already installed):
   ```bash
   poetry add --dev pytest-cov
   ```

2. **Update your test step** in `.github/workflows/dev-deploy.yaml`:
   ```yaml
   - name: Run tests with coverage
     run: |
       PYTHONPATH=$PWD poetry run pytest tests/ \
         --cov=cloud_functions \
         --cov-report=xml \
         --cov-report=term \
         --cov-fail-under=80
   ```

3. **Add the Codecov upload step** after your test step:
   ```yaml
   - name: Upload coverage to Codecov
     uses: codecov/codecov-action@v3
     with:
       token: ${{ secrets.CODECOV_TOKEN }} # Optional for public repos
       files: ./coverage.xml
       fail_ci_if_error: true
   ```

4. **Create a Codecov account** at https://codecov.io/ and connect it to your GitHub repository

5. **Create a codecov.yml** file in your repository root (optional but recommended):
   ```yaml
   coverage:
     status:
       project:
         default:
           target: 80%  # Adjust this percentage to your desired coverage threshold
           threshold: 1%  # Allow a 1% drop from the previous coverage
       patch:
         default:
           target: 80%
   ```

This setup will give you beautiful coverage charts on each PR, historical trends, and automatic comments showing coverage changes.

Alternative options:
- **Coveralls**: Similar to Codecov with a slightly different UI
- **GitHub Action artifacts**: Store the coverage report as an artifact without a third-party service

Would you like more specific details on any of these steps?

---

🏁 Script executed:
```
#!/bin/bash
# Check if pytest-cov is already in dependencies
grep -E "pytest-cov" pyproject.toml
```

Length of output: 57

---



</details>

@santoshgdev Great! I see you already have `pytest-cov = "^6.0.0"` in your dependencies, so we can skip the installation step. Here's how to integrate with Codecov:

1. **Update your test step** in `.github/workflows/dev-deploy.yaml` to generate coverage reports:
   ```yaml
   - name: Run tests with coverage
     run: |
       PYTHONPATH=$PWD poetry run pytest tests/ \
         --cov=cloud_functions \
         --cov-report=xml \
         --cov-report=term \
         --cov-fail-under=80
   ```

2. **Add the Codecov upload step** after your test step:
   ```yaml
   - name: Upload coverage to Codecov
     uses: codecov/codecov-action@v3
     with:
       files: ./coverage.xml
       fail_ci_if_error: false  # Set to true once integration is stable
   ```

3. **Sign up for Codecov**:
   - Go to https://codecov.io/
   - Sign in with your GitHub account
   - Authorize Codecov to access your repository
   - No token is required for public repositories

4. **Create a codecov.yml** file in your repository root for configuration:
   ```yaml
   coverage:
     status:
       project:
         default:
           target: 80%  # Your desired overall coverage target
           threshold: 1%  # Allow a 1% drop from the previous coverage
       patch:
         default:
           target: 80%  # Coverage required for new code
   comment:
     layout: "reach, diff, flags, files"
     behavior: default
     require_changes: false
   ```

After you push these changes, Codecov will automatically comment on your PRs with coverage reports and display trends over time. You'll get visual feedback on coverage changes directly in your GitHub workflow.

Would you like me to explain any part of this in more detail?</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-09T01:56:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Install pre-commit
+              run: pip install pre-commit
+
path: .github/workflows/dev-deploy.yaml
user: login: santoshgdev
body: how would the hash of the toml correspond to the python package version?
created_at: 2025-05-09T01:56:58Z
author_association: OWNER
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Install pre-commit
+              run: pip install pre-commit
+
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: @santoshgdev You're absolutely right to question this. My suggestion was incorrect and wouldn't work as intended. The `hashFiles()` function in GitHub Actions returns a hash of the file content, not the actual version number.

Here are better approaches for pinning the pre-commit version:

1. Pin directly in the workflow:
```yaml
run: pip install pre-commit==3.5.0  # Or your preferred version
```

2. Or if you have a version in pyproject.toml and want to dynamically extract it:
```yaml
- name: Extract pre-commit version
  id: precommit-version
  run: |
    VERSION=$(grep "pre-commit" pyproject.toml | sed -n 's/.*pre-commit.*=.*"\(.*\)".*/\1/p' || echo "3.5.0")
    echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Install pre-commit
  run: pip install pre-commit==${{ steps.precommit-version.outputs.version }}
```

The direct pinning approach is simpler and more reliable for CI/CD workflows.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: santoshgdev
PR: santoshgdev/fitnessllm-dataplatform#24
File: .github/workflows/dev-deploy.yaml:71-73
Timestamp: 2025-05-09T01:57:44.299Z
Learning: When suggesting version pinning in GitHub Actions workflows, use direct version specification (e.g., `pip install package==1.2.3`) rather than attempting to use `hashFiles()`, which returns a hash of the file content, not the version number itself.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@santoshgdev You're absolutely right to question this. My suggestion was incorrect and wouldn't work as intended. The `hashFiles()` function in GitHub Actions returns a hash of the file content, not the actual version number.

Here are better approaches for pinning the pre-commit version:

1. Pin directly in the workflow:
```yaml
run: pip install pre-commit==3.5.0  # Or your preferred version
```

2. Or if you have a version in pyproject.toml and want to dynamically extract it:
```yaml
- name: Extract pre-commit version
  id: precommit-version
  run: |
    VERSION=$(grep "pre-commit" pyproject.toml | sed -n 's/.*pre-commit.*=.*"\(.*\)".*/\1/p' || echo "3.5.0")
    echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Install pre-commit
  run: pip install pre-commit==${{ steps.precommit-version.outputs.version }}
```

The direct pinning approach is simpler and more reliable for CI/CD workflows.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: santoshgdev
PR: santoshgdev/fitnessllm-dataplatform#24
File: .github/workflows/dev-deploy.yaml:71-73
Timestamp: 2025-05-09T01:57:44.299Z
Learning: When suggesting version pinning in GitHub Actions workflows, use direct version specification (e.g., `pip install package==1.2.3`) rather than attempting to use `hashFiles()`, which returns a hash of the file content, not the version number itself.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-09T01:57:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,117 +1,119 @@
-import json
-from unittest.mock import patch
-
-import flask
-from firebase_admin import auth
-
-from cloud_functions.token_refresh.main import token_refresh
-from tests.cloud_functions.testing_utils import (
-    populate_inmemory_firestore_with_users_and_streams,
-)
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                response = token_refresh(mock_request)
-                assert response.status_code == 200
-                assert (
-                    "Token refreshed successfully for Strava"
-                    in json.loads(response.response[0])["message"]
-                )
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_missing_data_source(
-    mock_verify, mock_request, mock_decoded_token
-):
-    """Test token refresh with missing data source."""
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                mock_request.args = {}
-                response = token_refresh(mock_request)
-                assert response.status_code == 400
-                assert (
-                    "Required data_source parameter is missing"
-                    in json.loads(response.response[0])["message"]
-                )
-
-
-@patch(
-    "cloud_functions.token_refresh.main.partial_log_structured",
-    new=lambda *a, **kw: None,
-)
-@patch(
-    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
-    new=lambda *a, **kw: None,
-)
-@patch("firebase_admin.auth.verify_id_token")
-def test_token_refresh_invalid_token(mock_verify, mock_request, mock_decoded_token):
-    """Test token refresh with invalid token."""
-
-    # Setup in-memory Firestore with one user and strava stream
-    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
-    user_id = user_ids[0]
-    mock_decoded_token["uid"] = user_id
-    mock_verify.return_value = mock_decoded_token
-
-    with patch("google.cloud.firestore.Client", return_value=db):
-        app = flask.Flask(__name__)
-        with app.app_context():
-            with app.test_request_context(
-                path="/",
-                method="POST",
-                headers={"Authorization": "Bearer test_token"},
-                query_string={"data_source": "strava"},
-                json={},
-            ):
-                mock_verify.side_effect = auth.InvalidIdTokenError("Invalid token")
-                response = token_refresh(mock_request)
-                assert response.status_code == 401
-                assert (
-                    "Invalid Firebase ID Token; JWT Token Issu"
-                    in json.loads(response.response[0])["message"]
-                )
+import json
+from unittest.mock import patch
+
+import flask
+from firebase_admin import auth
+
+from cloud_functions.token_refresh.main import token_refresh
+from tests.cloud_functions.testing_utils import (
+    populate_inmemory_firestore_with_users_and_streams,
+)
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                response = token_refresh(mock_request)
path: tests/cloud_functions/token_refresh/test_main.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Use the actual Flask request inside the test context rather than `mock_request`.**

Currently, `token_refresh` is being called with `mock_request` (which is the patched `strava_refresh_oauth_token` mock) instead of the Flask request object. Inside an `app.test_request_context`, you should invoke:

```diff
@@ -38,7 +38,7 tests/cloud_functions/token_refresh/test_main.py
-                response = token_refresh(mock_request)
+                response = token_refresh(flask.request)
@@ -76,7 +76,7 tests/cloud_functions/token_refresh/test_main.py
-                response = token_refresh(mock_request)
+                response = token_refresh(flask.request)
@@ -113,7 +113,7 tests/cloud_functions/token_refresh/test_main.py
-                response = token_refresh(mock_request)
+                response = token_refresh(flask.request)
```

Also update each test function’s signature to remove `mock_request` (and rename the patch-injected mocks, e.g., `_mock_strava`, `_mock_logger`) so that the only arguments correspond to the mocks provided by the patch decorators.  



Also applies to: 76-76, 113-113

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T01:58:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -118,0 +1,119 @@
+import json
+from unittest.mock import patch
+
+import flask
+from firebase_admin import auth
+
+from cloud_functions.token_refresh.main import token_refresh
+from tests.cloud_functions.testing_utils import (
+    populate_inmemory_firestore_with_users_and_streams,
+)
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_success(mock_verify, mock_request, mock_decoded_token):
+    db, user_ids = populate_inmemory_firestore_with_users_and_streams(num_users=1)
+    user_id = user_ids[0]
+    mock_decoded_token["uid"] = user_id
+    mock_verify.return_value = mock_decoded_token
+
+    with patch("google.cloud.firestore.Client", return_value=db):
+        app = flask.Flask(__name__)
+        with app.app_context():
+            with app.test_request_context(
+                path="/",
+                method="POST",
+                headers={"Authorization": "Bearer test_token"},
+                query_string={"data_source": "strava"},
+                json={},
+            ):
+                response = token_refresh(mock_request)
+                assert response.status_code == 200
+                assert (
+                    "Token refreshed successfully for Strava"
+                    in json.loads(response.response[0])["message"]
+                )
+
+
+@patch(
+    "cloud_functions.token_refresh.main.structured_logger",
+    new=lambda *a, **kw: None,
+)
+@patch(
+    "cloud_functions.token_refresh.main.strava_refresh_oauth_token",
+    new=lambda *a, **kw: None,
+)
+@patch("firebase_admin.auth.verify_id_token")
+def test_token_refresh_missing_data_source(
+def test_token_refresh_missing_data_source(
path: tests/cloud_functions/token_refresh/test_main.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove duplicate function definition to fix syntax error.**

There are two identical `def test_token_refresh_missing_data_source(` lines, which causes a `SyntaxError: Expected an indented block after function definition`. Delete the redundant line so the signature is declared only once.

```diff
@@ tests/cloud_functions/token_refresh/test_main.py:55,56
- def test_token_refresh_missing_data_source(
- def test_token_refresh_missing_data_source(
+ def test_token_refresh_missing_data_source(
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
def test_token_refresh_missing_data_source(
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

55-55: SyntaxError: Expected ')', found newline

---

56-56: SyntaxError: Expected an indented block after function definition

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits b31e82a to 748bfd1
created_at: 2025-05-09T01:58:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
path: .github/workflows/dev-deploy.yaml
user: login: santoshgdev
body: will do
created_at: 2025-05-09T01:59:25Z
author_association: OWNER
diff_hunk: @@ -79,40 +58,74 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-
 
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: @santoshgdev Great! Thanks for considering the suggestion. This change should help ensure your pre-commit caches are both efficient and correctly aligned with your configuration.

---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: santoshgdev
PR: santoshgdev/fitnessllm-dataplatform#24
File: .github/workflows/dev-deploy.yaml:70-70
Timestamp: 2025-05-09T01:56:23.068Z
Learning: For GitHub Actions cache, using a more specific restore-key like `${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-` ensures that only caches matching the current pre-commit configuration are restored, which is important for correctness. Using a more general restore-key like `${{ runner.os }}-pre-commit-` optimizes for build speed but may restore less-relevant caches.
```

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@santoshgdev Great! Thanks for considering the suggestion. This change should help ensure your pre-commit caches are both efficient and correctly aligned with your configuration.

---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: santoshgdev
PR: santoshgdev/fitnessllm-dataplatform#24
File: .github/workflows/dev-deploy.yaml:70-70
Timestamp: 2025-05-09T01:56:23.068Z
Learning: For GitHub Actions cache, using a more specific restore-key like `${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-` ensures that only caches matching the current pre-commit configuration are restored, which is important for correctness. Using a more general restore-key like `${{ runner.os }}-pre-commit-` optimizes for build speed but may restore less-relevant caches.
```

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-09T01:59:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,113 +1,119 @@
-"""Strava specific utils."""
-import os
-from os import environ
-
-from beartype import beartype
-from beartype.typing import Any, Dict
-from google.cloud import firestore
-from stravalib.client import Client
-
-from ..shared.cloud_utils import get_secret
-from ..shared.logger_utils import create_structured_logger
-from ..shared.task_utils import encrypt_token
-from ..utils.task_utils import decrypt_token, update_last_refresh
-
-structured_logger = create_structured_logger(__name__)
-
-
-@beartype
-def strava_refresh_oauth_token(
-    db: firestore.Client, uid: str, refresh_token: str
-) -> None:
-    """Call Strava OAuth to refresh the token.
-
-    Args:
-        db: Firestore client.
-        uid: Firestore user id.
-        refresh_token: Encrypted Strava OAuth refresh token.
-
-    Raises:
-        ValueError: If refresh token is invalid.
-    """
-    structured_logger(message="Starting token refresh", uid=uid)
-
-    encryption_key = get_secret(os.environ["ENCRYPTION_SECRET"])["token"]
-
-    client = Client()
-    strava_secret = get_secret(environ["STRAVA_SECRET"])
-    client_id = strava_secret.get("client_id")
-    client_secret = strava_secret.get("client_secret")
-
-    if not client_id or not client_secret:
-        structured_logger(message="Strava credentials not found", level="ERROR")
-        raise ValueError("Strava credentials not found in Secret Manager")
-
-    try:
-        token_response = client.refresh_access_token(
-            client_id=client_id,
-            client_secret=client_secret,
-            refresh_token=decrypt_token(refresh_token, encryption_key),
-        )
-        structured_logger(message="Token refresh successful", uid=uid)
-
-        new_tokens = {
-            "accessToken": encrypt_token(
-                token_response["access_token"], encryption_key
-            ),
-            "refreshToken": encrypt_token(
-                token_response["refresh_token"], encryption_key
-            ),
-            "expiresAt": token_response["expires_at"],
-            "lastTokenRefresh": update_last_refresh(),
-        }
-
-        strava_update_user_tokens(db=db, uid=uid, new_tokens=new_tokens)
-        structured_logger(message="Tokens updated in Firestore", uid=uid)
-    except Exception as e:
-        structured_logger(
-            message="Error refreshing token",
-            uid=uid,
-            error=str(e),
-            level="ERROR",
-        )
-        raise
-
-
-# Bear type is removed here due to a test that has a testing component located in tests/.
-# This is done so that the testing components don't need to be shipped with the production code.
-def strava_update_user_tokens(
-    db: firestore.Client, uid: str, new_tokens: Dict[str, Any]
-) -> None:
-    """Update user document with new tokens.
-
-    Args:
-        db: Firestore client.
-        uid: Firestore user id.
-        new_tokens: New tokens.
-    """
-    structured_logger(message="Updating user tokens", uid=uid)
-
-    strava_ref = (
-        db.collection("users").document(uid).collection("stream").document("strava")
-    )
-
-    doc = strava_ref.get()
-    if not doc.exists:
-        structured_logger(
-            message="Strava document doesn't exist in stream subcollection",
-            uid=uid,
-            level="ERROR",
-        )
-        # Create the document with default values
-        strava_ref.set(new_tokens, merge=True)
-        return
-
-    strava_ref.update(
-        {
-            "accessToken": new_tokens["accessToken"],
-            "refreshToken": new_tokens["refreshToken"],
-            "expiresAt": new_tokens["expiresAt"],
-            "lastTokenRefresh": new_tokens["lastTokenRefresh"],
-        }
-    )
-    structured_logger(message="User tokens updated successfully", uid=uid)
+"""Strava specific utils."""
+import os
+from os import environ
+
+from beartype import beartype
+from beartype.typing import Any, Dict
+from google.cloud import firestore
+from stravalib.client import Client
+
+from ..shared.cloud_utils import get_secret
+from ..shared.logger_utils import create_structured_logger
+from ..shared.task_utils import encrypt_token
+from ..utils.task_utils import decrypt_token, update_last_refresh
+
+structured_logger = create_structured_logger(__name__)
+
+
+@beartype
+def strava_refresh_oauth_token(
+    db: firestore.Client,
+    uid: str,
+    refresh_token: str,
+) -> None:
+    """Call Strava OAuth to refresh the token.
+
+    Args:
+        db: Firestore client.
+        uid: Firestore user id.
+        refresh_token: Encrypted Strava OAuth refresh token.
+
+    Raises:
+        ValueError: If refresh token is invalid.
+    """
+    structured_logger(message="Starting token refresh", uid=uid)
+
+    encryption_key = get_secret(os.environ["ENCRYPTION_SECRET"])["token"]
+
+    client = Client()
+    strava_secret = get_secret(environ["STRAVA_SECRET"])
path: cloud_functions/token_refresh/streams/strava.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Consistency: Use a single import for environment variables**  
You’re using both `os.environ[...]` and the imported `environ[...]`. For consistency, choose one approach (e.g. always use `environ[...]`) to keep your code uniform.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T02:14:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,113 +1,119 @@
-"""Strava specific utils."""
-import os
-from os import environ
-
-from beartype import beartype
-from beartype.typing import Any, Dict
-from google.cloud import firestore
-from stravalib.client import Client
-
-from ..shared.cloud_utils import get_secret
-from ..shared.logger_utils import create_structured_logger
-from ..shared.task_utils import encrypt_token
-from ..utils.task_utils import decrypt_token, update_last_refresh
-
-structured_logger = create_structured_logger(__name__)
-
-
-@beartype
-def strava_refresh_oauth_token(
-    db: firestore.Client, uid: str, refresh_token: str
-) -> None:
-    """Call Strava OAuth to refresh the token.
-
-    Args:
-        db: Firestore client.
-        uid: Firestore user id.
-        refresh_token: Encrypted Strava OAuth refresh token.
-
-    Raises:
-        ValueError: If refresh token is invalid.
-    """
-    structured_logger(message="Starting token refresh", uid=uid)
-
-    encryption_key = get_secret(os.environ["ENCRYPTION_SECRET"])["token"]
-
-    client = Client()
-    strava_secret = get_secret(environ["STRAVA_SECRET"])
-    client_id = strava_secret.get("client_id")
-    client_secret = strava_secret.get("client_secret")
-
-    if not client_id or not client_secret:
-        structured_logger(message="Strava credentials not found", level="ERROR")
-        raise ValueError("Strava credentials not found in Secret Manager")
-
-    try:
-        token_response = client.refresh_access_token(
-            client_id=client_id,
-            client_secret=client_secret,
-            refresh_token=decrypt_token(refresh_token, encryption_key),
-        )
-        structured_logger(message="Token refresh successful", uid=uid)
-
-        new_tokens = {
-            "accessToken": encrypt_token(
-                token_response["access_token"], encryption_key
-            ),
-            "refreshToken": encrypt_token(
-                token_response["refresh_token"], encryption_key
-            ),
-            "expiresAt": token_response["expires_at"],
-            "lastTokenRefresh": update_last_refresh(),
-        }
-
-        strava_update_user_tokens(db=db, uid=uid, new_tokens=new_tokens)
-        structured_logger(message="Tokens updated in Firestore", uid=uid)
-    except Exception as e:
-        structured_logger(
-            message="Error refreshing token",
-            uid=uid,
-            error=str(e),
-            level="ERROR",
-        )
-        raise
-
-
-# Bear type is removed here due to a test that has a testing component located in tests/.
-# This is done so that the testing components don't need to be shipped with the production code.
-def strava_update_user_tokens(
-    db: firestore.Client, uid: str, new_tokens: Dict[str, Any]
-) -> None:
-    """Update user document with new tokens.
-
-    Args:
-        db: Firestore client.
-        uid: Firestore user id.
-        new_tokens: New tokens.
-    """
-    structured_logger(message="Updating user tokens", uid=uid)
-
-    strava_ref = (
-        db.collection("users").document(uid).collection("stream").document("strava")
-    )
-
-    doc = strava_ref.get()
-    if not doc.exists:
-        structured_logger(
-            message="Strava document doesn't exist in stream subcollection",
-            uid=uid,
-            level="ERROR",
-        )
-        # Create the document with default values
-        strava_ref.set(new_tokens, merge=True)
-        return
-
-    strava_ref.update(
-        {
-            "accessToken": new_tokens["accessToken"],
-            "refreshToken": new_tokens["refreshToken"],
-            "expiresAt": new_tokens["expiresAt"],
-            "lastTokenRefresh": new_tokens["lastTokenRefresh"],
-        }
-    )
-    structured_logger(message="User tokens updated successfully", uid=uid)
+"""Strava specific utils."""
+import os
+from os import environ
+
+from beartype import beartype
+from beartype.typing import Any, Dict
+from google.cloud import firestore
+from stravalib.client import Client
+
+from ..shared.cloud_utils import get_secret
+from ..shared.logger_utils import create_structured_logger
+from ..shared.task_utils import encrypt_token
+from ..utils.task_utils import decrypt_token, update_last_refresh
+
+structured_logger = create_structured_logger(__name__)
+
+
+@beartype
+def strava_refresh_oauth_token(
+    db: firestore.Client,
+    uid: str,
+    refresh_token: str,
+) -> None:
+    """Call Strava OAuth to refresh the token.
+
+    Args:
+        db: Firestore client.
+        uid: Firestore user id.
+        refresh_token: Encrypted Strava OAuth refresh token.
+
+    Raises:
+        ValueError: If refresh token is invalid.
+    """
+    structured_logger(message="Starting token refresh", uid=uid)
+
+    encryption_key = get_secret(os.environ["ENCRYPTION_SECRET"])["token"]
+
+    client = Client()
+    strava_secret = get_secret(environ["STRAVA_SECRET"])
+    client_id = strava_secret.get("client_id")
+    client_secret = strava_secret.get("client_secret")
+
+    if not client_id or not client_secret:
+        structured_logger(message="Strava credentials not found", level="ERROR")
+        raise ValueError("Strava credentials not found in Secret Manager")
+
+    try:
+        token_response = client.refresh_access_token(
+            client_id=client_id,
+            client_secret=client_secret,
+            refresh_token=decrypt_token(refresh_token, encryption_key),
+        )
+        structured_logger(message="Token refresh successful", uid=uid)
+
+        new_tokens = {
+            "accessToken": encrypt_token(
+                token_response["access_token"],
+                encryption_key,
+            ),
+            "refreshToken": encrypt_token(
+                token_response["refresh_token"],
+                encryption_key,
+            ),
+            "expiresAt": token_response["expires_at"],
+            "lastTokenRefresh": update_last_refresh(),
+        }
+
+        strava_update_user_tokens(db=db, uid=uid, new_tokens=new_tokens)
+        structured_logger(message="Tokens updated in Firestore", uid=uid)
+    except Exception as e:
+        structured_logger(
+            message="Error refreshing token",
+            uid=uid,
+            error=str(e),
+            level="ERROR",
+        )
+        raise
+
+
+# Bear type is removed here due to a test that has a testing component located in tests/.
+# This is done so that the testing components don't need to be shipped with the production code.
+def strava_update_user_tokens(
+    db: firestore.Client,
+    uid: str,
+    new_tokens: Dict[str, Any],
+) -> None:
+    """Update user document with new tokens.
+
+    Args:
+        db: Firestore client.
+        uid: Firestore user id.
+        new_tokens: New tokens.
+    """
+    structured_logger(message="Updating user tokens", uid=uid)
+
+    strava_ref = (
+        db.collection("users").document(uid).collection("stream").document("strava")
+    )
+
+    doc = strava_ref.get()
+    if not doc.exists:
+        structured_logger(
+            message="Strava document doesn't exist in stream subcollection",
+            uid=uid,
+            level="ERROR",
+        )
+        # Create the document with default values
+        strava_ref.set(new_tokens, merge=True)
+        return
+
+    strava_ref.update(
+        {
+            "accessToken": new_tokens["accessToken"],
+            "refreshToken": new_tokens["refreshToken"],
+            "expiresAt": new_tokens["expiresAt"],
+            "lastTokenRefresh": new_tokens["lastTokenRefresh"],
+        }
+    )
path: cloud_functions/token_refresh/streams/strava.py
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Nitpick: Add trailing comma after dict literal argument**  
PEP 8 recommends a trailing comma for multi-line arguments. Please add a comma after the closing `}` in the `strava_ref.update` call:  
```diff
 strava_ref.update(
-    {
+    {
         "accessToken": new_tokens["accessToken"],
         "refreshToken": new_tokens["refreshToken"],
         "expiresAt": new_tokens["expiresAt"],
         "lastTokenRefresh": new_tokens["lastTokenRefresh"],
     }
+    ,
 )
```  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    strava_ref.update(
        {
            "accessToken": new_tokens["accessToken"],
            "refreshToken": new_tokens["refreshToken"],
            "expiresAt": new_tokens["expiresAt"],
            "lastTokenRefresh": new_tokens["lastTokenRefresh"],
        }
        ,
    )
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

117-117: Trailing comma missing

Add trailing comma

(COM812)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T02:14:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,93 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-
+
+            - name: Install pre-commit
+              run: pip install pre-commit
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Run pre-commit checks
+              run: pre-commit run --all-files
+
+            - name: Copy shared code into each function directory
               run: |
-                  pip install pre-commit
-                  pre-commit install
-                  # Run once to populate the cache
-                  pre-commit run --all-files || true
-
-            - name: Save pre-commit cache
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
-              uses: actions/cache/save@v4
+                  for fn in cloud_functions/*; do
+                    if [ -d "$fn" ] && [ "$(basename $fn)" != "shared" ]; then
+                      rm -rf "$fn/shared"
+                      mkdir -p "$fn/shared"
+                      cp -r cloud_functions/shared/. "$fn/shared/"
+                    fi
+                  done
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Refactor: DRY copy-shared-code step**  
The shell loop syncing `cloud_functions/shared` is duplicated later. Extract this into a reusable step or composite action to avoid maintenance drift.

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 30a1f20 to ece2f74
created_at: 2025-05-09T02:14:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,46 +18,17 @@ env:
     REGION: us-west1
     SERVICE_NAME: fitnessllm-dp
     ENVIRONMENT: dev
-    PYTHONPATH: ${{ github.workspace }}
     PROJECT_ID: ${{ secrets.PROJECT_ID }}
     ENCRYPTION_SECRET: ${{ secrets.ENCRYPTION_SECRET }}
     STRAVA_SECRET: ${{ secrets.STRAVA_SECRET }}
     INFRASTRUCTURE_SECRET: ${{ secrets.INFRASTRUCTURE_SECRET }}
+    PYTHONPATH: ${{ github.workspace }}
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Scope PYTHONPATH to relevant steps**  
Defining `PYTHONPATH` at the global `env` level may affect jobs that don’t need it. Consider moving this into the `testing` job (or other specific steps) to limit side effects.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T02:14:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,93 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-
+
+            - name: Install pre-commit
+              run: pip install pre-commit
 
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Pin pre-commit version for repeatability**  
Installing `pre-commit` without a version may lead to unexpected upgrades. Consider pinning to your project’s locked version, e.g.:  
```yaml
run: pip install pre-commit==3.5.0
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T02:14:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,93 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-
+
+            - name: Install pre-commit
+              run: pip install pre-commit
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Run pre-commit checks
+              run: pre-commit run --all-files
+
+            - name: Copy shared code into each function directory
               run: |
-                  pip install pre-commit
-                  pre-commit install
-                  # Run once to populate the cache
-                  pre-commit run --all-files || true
-
-            - name: Save pre-commit cache
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
-              uses: actions/cache/save@v4
+                  for fn in cloud_functions/*; do
+                    if [ -d "$fn" ] && [ "$(basename $fn)" != "shared" ]; then
+                      rm -rf "$fn/shared"
+                      mkdir -p "$fn/shared"
+                      cp -r cloud_functions/shared/. "$fn/shared/"
+                    fi
+                  done
+
+            - name: Run tests with coverage
+              run: |
+                  PYTHONPATH=$PWD poetry run pytest tests/ \
+                    --cov-branch \
+                    --cov=cloud_functions \
+                    --cov-report=xml \
+                    --cov-report=term \
+                    --junitxml=junit.xml \
+                    -o junit_family=legacy
+
+            - name: Upload coverage to Codecov
+              uses: codecov/codecov-action@v3
               with:
-                  path: |
-                      ~/.cache/pre-commit
-                      ~/.cache/pip
-                  key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
+                  files: ./coverage.xml
+                  fail_ci_if_error: false
+                  token: ${{ secrets.CODECOV_TOKEN }}
 
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Update Codecov Action version**  
`codecov/codecov-action@v3` is flagged by actionlint as outdated. Upgrade to the latest `@v4` release to ensure compatibility:  
```yaml
uses: codecov/codecov-action@v4
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 actionlint (1.7.4)</summary>

98-98: the runner of "codecov/codecov-action@v3" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits 30a1f20 to 18d1992
created_at: 2025-05-09T02:14:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,40 +58,93 @@ jobs:
               run: |
                   poetry install --no-interaction
 
-            - name: Run pytest
-              run: poetry run pytest tests/
-
             - name: Cache pre-commit
-              id: cache-pre-commit
               uses: actions/cache@v4
               with:
                   path: |
                       ~/.cache/pre-commit
                       ~/.cache/pip
                   key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ hashFiles('**/poetry.lock') }}
                   restore-keys: |
-                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-${{ runner.os }}-pre-commit-
+                      ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}-
+
+            - name: Install pre-commit
+              run: pip install pre-commit
 
-            - name: Install and initialize pre-commit
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
+            - name: Run pre-commit checks
+              run: pre-commit run --all-files
+
+            - name: Copy shared code into each function directory
               run: |
-                  pip install pre-commit
-                  pre-commit install
-                  # Run once to populate the cache
-                  pre-commit run --all-files || true
-
-            - name: Save pre-commit cache
-              if: steps.cache-pre-commit.outputs.cache-hit != 'true'
-              uses: actions/cache/save@v4
+                  for fn in cloud_functions/*; do
+                    if [ -d "$fn" ] && [ "$(basename $fn)" != "shared" ]; then
+                      rm -rf "$fn/shared"
+                      mkdir -p "$fn/shared"
+                      cp -r cloud_functions/shared/. "$fn/shared/"
+                    fi
+                  done
+
+            - name: Run tests with coverage
+              run: |
+                  PYTHONPATH=$PWD poetry run pytest tests/ \
+                    --cov-branch \
+                    --cov=cloud_functions \
+                    --cov-report=xml \
+                    --cov-report=term \
+                    --junitxml=junit.xml \
+                    -o junit_family=legacy
+
path: .github/workflows/dev-deploy.yaml
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Optional: Enforce coverage thresholds**  
You could add `--cov-fail-under=80` (or your target) to the pytest command to enforce a minimum coverage in CI.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-09T02:14:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-09T01:38:31Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: rate limited by coderabbit.ai -->

> [!WARNING]
> ## Rate limit exceeded
> 
> @santoshgdev has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait **9 minutes and 14 seconds** before requesting another review.
> 
> <details>
> <summary>⌛ How to resolve this issue?</summary>
> 
> After the wait time has elapsed, a review can be triggered using the `@coderabbitai review` command as a PR comment. Alternatively, push new commits to this PR.
> 
> We recommend that you space out your commits to avoid hitting the rate limit.
> 
> </details>
> 
> 
> <details>
> <summary>🚦 How do rate limits work?</summary>
> 
> CodeRabbit enforces hourly rate limits for each developer per organization.
> 
> Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.
> 
> Please see our [FAQ](https://docs.coderabbit.ai/faq) for further information.
> 
> </details>
> 
> <details>
> <summary>📥 Commits</summary>
> 
> Reviewing files that changed from the base of the PR and between dfa3142c938569b27c4f465164a815c7c86b3a02 and d9b87d1b9660beb02e5ab2dbfa81a4cb71d4905f.
> 
> </details>
> 
> <details>
> <summary>📒 Files selected for processing (5)</summary>
> 
> * `.codecov.yaml` (1 hunks)
> * `.github/actions/copy-shared/action.yaml` (1 hunks)
> * `.github/workflows/dev-deploy.yaml` (5 hunks)
> * `README.md` (1 hunks)
> * `cloud_functions/token_refresh/streams/strava.py` (1 hunks)
> 
> </details>

<!-- end of auto-generated comment: rate limited by coderabbit.ai -->
<!-- walkthrough_start -->

## Summary by CodeRabbit

- **Chores**
  - Removed a legacy continuous integration workflow and restructured deployment workflows for improved modularity, caching, and test coverage reporting.
  - Updated the Makefile to ensure shared code is copied before building, testing, or running cloud functions.
  - Adjusted Python dependencies, moving several packages to main dependencies and updating version constraints.
- **Style**
  - Reformatted code and tests for improved readability; no changes to logic or functionality.
- **Tests**
  - Updated test utilities for timezone handling and improved patching in token refresh tests.
  - Added missing package initializers to enable proper test discovery.
- **New Features**
  - None.
- **Bug Fixes**
  - None.
## Summary by CodeRabbit

- **Chores**
  - Removed an obsolete continuous integration workflow for branch checks.
  - Improved development deployment workflow with clearer separation of testing, change detection, and deployment phases, along with enhanced caching and environment setup.
  - Updated the Makefile to ensure shared code is always copied before building or testing cloud functions.

- **Documentation**
  - Added initializer files to shared and test directories for better package recognition.

- **Style**
  - Reformatted several code and test files without altering functionality.

- **Tests**
  - Updated test imports for timezone handling and logging patches to improve clarity and maintainability.

- **Dependencies**
  - Updated and reorganized project dependencies, moving some to main dependencies and adjusting version constraints.

## Walkthrough

This set of changes restructures CI/CD workflows by deleting the CI workflow, modularizing the deployment workflow into separate testing, change detection, and deployment jobs, and improving shared code management. It also updates dependency specifications, introduces package initializers, adjusts Makefile targets, and applies minor formatting and import improvements across several files.

## Changes

| File(s)                                                                                      | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
|----------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| .github/workflows/ci.yaml                                                                    | Deleted the CI workflow, which previously ran pre-commit checks and tests on branch pushes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| .github/workflows/dev-deploy.yaml                                                            | Replaced `check_changes` and `cloud_run_deploy` jobs with `testing`, `cloud_run_job_detect_changes`, and `cloud_run_job_deploy` jobs; improved change detection, modularized workflow phases, and enhanced environment and caching setup.                                                                                                                                                                                                                                                                                                                                                                              |
| Makefile                                                                                     | Added `make copy_shared` as a prerequisite for the `build`, `test`, and `run` targets to ensure shared code is copied before Docker operations.                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| cloud_functions/shared/__init__.py<br>tests/cloud_functions/token_refresh/streams/__init__.py | Added empty `__init__.py` files with docstrings to enable package recognition for the `shared` and test streams directories.                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| cloud_functions/shared/cloud_utils.py<br>cloud_functions/shared/logger_utils.py<br>cloud_functions/shared/task_utils.py<br>cloud_functions/token_refresh/main.py<br>cloud_functions/token_refresh/streams/strava.py<br>tests/cloud_functions/token_refresh/streams/test_strava.py | Reformatted files with whitespace and line-ending adjustments; no changes to logic, control flow, or error handling.                                                                                                                                                                                                                                                                                                                                                                                                                    |
| pyproject.toml                                                                               | Changed Python version specifier, downgraded `numpy`, moved several packages from dev to main dependencies, added `flask`, and removed the `[tool.poetry.dev-dependencies]` section.                                                                                                                                                                                                                                                                                                                                                                                            |
| tests/cloud_functions/testing_utils.py                                                       | Changed timezone assignment in `populate_inmemory_firestore_with_users_and_streams` to use `datetime.now(timezone.utc)` instead of replacing tzinfo.                                                                                                                                                                                                                                                                                                                                                                                                                            |
| tests/cloud_functions/token_refresh/test_main.py                                             | Updated test mocks to patch `structured_logger` instead of `partial_log_structured`; test logic and assertions remain unchanged.                                                                                                                                                                                                                                                                                                                                                                                                        |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Dev as Developer
    participant GitHub as GitHub Actions
    participant Test as Testing Job
    participant Detect as Change Detection Job
    participant Deploy as Deployment Job
    participant GCP as Google Cloud Platform

    Dev->>GitHub: Push code to repository
    GitHub->>Test: Run testing job (checkout, setup, copy shared, pre-commit, pytest)
    Test-->>GitHub: Report test results
    GitHub->>Detect: Run change detection job (analyze changed files)
    Detect-->>GitHub: Output changes_detected boolean
    GitHub->>Deploy: Conditionally run deployment job if changes detected or forced
    Deploy->>GCP: Authenticate, build, push Docker image, deploy Cloud Run job
    Deploy-->>GitHub: Report deployment status
```

## Possibly related PRs

- **#19**: Both PRs restructure `.github/workflows/dev-deploy.yaml`, focusing on deployment workflow modularization and change detection.
- **#23**: This PR builds upon earlier modularization, expanding the workflow into multiple jobs for testing, detection, and deployment.
- **#20**: Both PRs involve deleting CI workflows and improving shared code handling and deployment logic in `.github/workflows/dev-deploy.yaml`.

## Poem

> 🐇  
> Hopped through workflows, made them neat,  
> Split the jobs—a tidy feat!  
> Shared code copied, tests in tow,  
> Dependencies lined up in a row.  
> With Makefile tweaks and Python’s tune,  
> This rabbit’s ready—deployments soon!  
> ✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEejqANiS4AJfLgBm8AB4B6AGIBJaGAAMvsCdnMAZ4BloDADlsZgFKLgAmABYDAFVEeMhETAJEWCIlCSN9Y3AoMnp8BxwCYjJlGnomVnYuXn5hUXEpGXkmJSpVdS0dEpMoOFRUTBrCUnIqRoUWjE5IKgB3LJjmNAp5OQUBlTVNbV0wQ1LTAw0idVhsAVcN/AoAawcLfA3EV1CNLI0MwLBwDAAiSEGADE0MgAEFPHUFtQ6NtWHt5FVGLBMKREGZYCRIEorEsnFZIBs0Mg0JAAOLqGxPBFieD4DDIV4fL4/BQYJxEbCLdlYDDAtHggDCnkgUqJDHeiHBJOk8CI5HoBHW2CwTFW8Aw2Hw2GQhpoRBFHJxoiV/Cw3FNROQ2oEVAwDGdkF2uE9hqIkFwRJ41BoFCwAHIAFReHxRqMRjQwYPcz7fLZKJya9BZf2UoSCRhoCxWejggCO2GL6lkIQVSpVQeokBIzlEeDR1pmqQEutw2EgFlRiFwOowCyTnlHGVHQeJAAUAJrQGwAeUi8/hK5bGAk8AoHLYq0gEj28BUlO1c4ZTJZqcQ3DQDGJtH3XTesg0hOJBYE/MQSBLNiGRSFQFhZCQlbsOe4EjiQ3CIFwnq2v6/B4IGwYUPB+AAQQ+xHCQAA0EG4OIGABtg3CQPOshBtaADMGgAIwJBoCRUg8PDwFRDBPgg5E7hedDEbxfoCdevAkCELDMOogb4PgFgRsgonBq+WFiB+kDvCQshooc164nk/DVBJWHSawcn6oKwrUKKkAUkRKCcrgxYWKhZn4CIYjoBgWrSGRAZKNwFRkKE0iQAAFIaDAWNgr4CZJFmyaOmD0NwtEBQAlMRFC6hgqFuTw5nNClkCwApdobJxr4OA4yB5D8WDWg42hxVhxFpQ5hpufIeXjqhGU0CO9oYcSw24Mg6nvvsX4GJMXJvGmfJkIgwpov0xKVtWuAHPIaB4Cwdm8SWfX5YNJUyXJFX4HaXUTcg1pYVYp7Hm6mDIcglHWo6xkkFIqyIHNRiwgiFhhnZHIuvgY2qrFeyQ5yJkts43BvEBfCOgI7kMDu4jiNIRiRByJBfpC4LFDcdwPE8LxLbyvyuIUYDBd8n5AiCYLkzCcKIsiDRomtGL4dinp4oT80pvT6ZUjS6wkK1mlYfQhxYcw+B7uJwZvOqPXgQABshioAPpi+R0j6759CG988XG/1xus/gsiW7+tJ+fL3BDqEWskMwHFBjm5BbMw2Dg/AYC/lkuB5WI61JvCkDB5A+sTf6ruFtSZqrAetDYM+Wq4qOIUUA4bzMCpijEkb7wmrgxE0XRWAzpRDf4CQMfyIaI5uYjAewEWYkBti157hQ/bFjuY+HuwxHBaFHpdy5vfiByxFJaVcnd65JZ911rbtqvGAifgGWoXkewbVXznag4upsta01K/A0idR7/UFeJAVcpxTCgWgpBPbo0Kh7KUVc/6QEot8NAtAkxwGJKnAK6dIBRwwDDb45s+D9WQENAK0c0rFhJpAXso5zQkEtMOBQ/9AH3TwVhNa4NIFe3wDAoGRhE7JxtiaWg9tdTG1/I7DuXRTa4nNogDOf4s7oFoEoWgxENgIE9KqEKflHpYEQSOdOk5Rw12QGXLBJAXo5G6pSM2+JiF9BYI+CgHlgx3xLJAe4o4EAji0nEXAGwSBkFhpvUccR9HjWDL9Ae91gxDgmssFKxEYpxQSgGfW1Mgy00tk/PCS9YZKBoA/B0oZKAYG0WhXAjpJo5gEApKwMxDaiPxIIrJjRLZfAAXNDhJAthcLtg7ARTsXYoMLPPVRo1ryZK6PZKOXVsH8lfEfXqKBqhmIipfVUdTOx8HgNUOk+iC7KLZigF0NiiCkGVgUku+iK42kVHXTqeAiQGl4sNfuDIFJEEpFKW2ciSJfSoq87hkAADKAARAA0lbSA/z8CKkoJAAAQtgeAFhaDOBEnxCKYKIV8CHHpCgiBiK9jhbQd26UnQLNBeCnSqzdiAO1PCceaynyjgAErkMAvsN+9Bum0jlG8yA9LdS9MkZxMg08MBHlHKeGxQk2GSwQUuFc65NwrktoK/cM9jxivPNjYkUj1ZSBVoYvk9giR8AyAwLCJTDSwyVQeYV7AILZLgcGVSqEdKyD0W8YqUlfFUkoMSACzAvZrJfrA5MxIN5XVIcvXeR8QUHwYHgeycEEJeqwhBaxqJ6BdXgH6g8Or7XV1PrIVCwFcTKwIjfGGd8PRRtSTrCKWqNZonNDDa8GjAoSNzfwfZesU41xEeLcRg58D3FxnWnVZbYbJ2GdkvlDyYGyNVL2YeeBikgpmK2R8fk0SORDIHR8pE8kOTdfMpZIyORzXgd2t5xsK3ZNqcI+Z/bUF8n6fi0aLatGQFXP/EsxFrz3u9IoMOZ4ABegTNXSz5I2kkgEYqziQeRES1SXxCOyay7ZzsRU8CMrWzimbeD1qaHxf0IkKlYT4JalV04O6tx3KIrZR7J1Ru+EO1DZCKFLAiX/ZQgCsJoxpeROaoN4TgwaKKaGGTRBDitEjbEa70YrJ4E8HGeN1AvwJAYKAAApQsat8NcCqbaXtYjLbmoSU4x4zxUwMz+MzbpgJgQWH1upyAWm/w6Z1Xp2K3DeEYEEcwnpJnEnmbpjydM1n/os3gmzOzIJHOacLLOugem07kWM+owLtNLOhaZuF2zHMHNOZc9I2RHnL2dMELesQhn8SpZTuliz4HGY2ci87aL+W4t/gS7QErXmysCF82zGrpmab1ZCz8MLEgIt+da7F5zhYAIamoOtJhtA03dbtteo+FXcBVYts5WrZmMsNfG5NqLeXLaABQCIraJn1hSxOo5LRAJEQihOpm4ABZNAOlHJcyhIJpE8wBb0CFrsEWczENqfPR9r7cLNVy3VrVQN8kitB1ae6rClYkDqB9TQKi+tdg6QUBlY2F9laW38W8UDKdaCkshbi+FltSoZqwM2untBLauQoKQeuILyfJubdTtFY4GcyTStnWGLbLZdX1v1dnewudA2TKgVa60XRF1hiT67b4lZLwAkoFsfFGBco2/Gp41b8KoEoitpYhwmBnwEg4A8/t2k8ON1DVwGu2fEIVhT4hsL4W2OJKislKAKXEjdR/Dy38QVkKoGyKQEFEAAShu2w2+bifFroLLznHcUCTUMdUKRxYsIwPkJmQ0aJxl+20Eje+iGg3E34HOdFg6wgdsiaLpNxJixhjoMDHmYMIZHzE0MiTCMh/I1k+PeTWMlPQQJmpqA8IZFojx59vNROPeS45ZJdHsLcLY/gntlnfvPcc651+Rfy/rb4/X7IdPl9PdyzpDvyCe+sfR0P+a5tE0s/n6c0vudVfAnW3O/TfdAbfUjV/THGgD/KiL/YMaXXUX/DuMmF7CAMAIwTzdbe+Ifd3DPWgVwY2Y2Q0dQIgjQDKH7CmP7fmRYQWHYTEZGe9dhJOVHLdZ3K9HA0TPAh/Qg4ggqbbY2cgnpIyL3bxTrfkVyEggSDkCwfaXMciSkdycgGdf9POKwMAF6QxEkcFEcGxASfWcETwfgjQcEBJRXPRGHCCCgHocA3yZyFTasEDPgfRcXMAx8RUABYkEhbQiKNBcNGOADZ8XyeQV3cUcCTaYjGkDIbFNvUIyVEGXmYTKTYfNSUfZIifVGOTSoTGRTVvOfVTImEmVAimV7DAgwLAl3Tgt3D3f4S9ONCwIGCg57Kg3mf7eoWgoHeg0HHEPtb8ZTZNNgiojgytLgmooY+oxonpKRLCU5XJegaqOidCTAPocHJHa8JjMIE+HORSBydMYiN1SgA8PgURWgJQogdtTaaOWOfsDqEPXjSaYiUI1ULMFTNeaNZwZ8bgKNE4s4+WXYc1eAJQW5YsJMBvUIyeeHANO5UTTvb0GBcaJtBArnYnUQU1BpKo5qPgZYxvQ1EMceZGa8RyPvQTJIxGFIpDeGdImTTIqfbIhTbGPIg0efQo8gYoymMooYuI7g5WVwJjQ5Y2CYoQyg/vPmAHDo9EEHLEMHXoqVKDOqCQ6vZANBByDEyeN1DYk6HosRdtGQ+QSEpwaE60VAa8WdV45qdZQMKgOFVCRnI/BAvk/0DQdwcuXJCgBpF06cGOQqTnGIG1BY/iVwqjbgY2PkygdEkY09cwrUwBanXw+wdAOqLoWGOIXEPcN4LYgI8CBmfYsjCgI48qNKX4keB1KuBOBxA1SFCI5yWKeKVCXDdGGIp4svfg0TFjVYPJSeDU3KKvbuFAIE8QE6ATRIwfGEq8VIykskjI+46fXI3GfIiWYmVk5o9kzAy9Lkmo1yRAd4AU8QBooU5c6gsUtNCUhg0WcHPov9bUfWMKfYL442AgHSDASXZNR0Z6eQEcWQdyTRXGLqWY0if0MAZ6NNdtDUg9PgfUUCQKWGZ1JHOQe5KRX1f1JwTdR3OwtZMAQxDIYhb4RUJHOkACBQ4kQ0JQvNPyM0yeNdehJPfJBEEkKBMIY80qG1FQOufkSCgPbSXSWCzKLkOWc0XOfOfSdCWMpU+MtARMnycE9yXadtR8GRG0oqaY73CgH0djGGGJeKLvS0tqG0kXFdQccvYhYvd4MC2GOXX01YUEmGP9TxZNXYPXMc4kG82QL4+yPDZ8RPdeU0+DSATwAANUcXaMRhzMKWXT/MCiTE/WUG/VhieJTLQDTIMX+KRkBOgkHISIHxEyhjWPHMk0nOpOnLpJn0ZPxgKKiCKOXNKNXK8y5IfLIHtgVnoVgFcGSv3O5kPKCqWGB1POlLET6OggGMsPYNqtunqpmKapauryEMtiUq9ifDRH9JRy2FAmoowmbFQCktkL7LSsnkco7V1jCNzWTUWWVIyF2FuTbw1Mnj/XgLzSUHbXrPHhiJIPEEcL7mxHcDfAEBpGJCX1kmPiuPzhuLRAdIEhbm4FQ2/1Gp83GukFgEthsGgGgHnBVIjKwDVmr3VwlHbSeM0QcR+IiilFXHpV+XdS+HVFgFHFXHnGgE8HXFJt32/mIlbBjjpQJRPEoADQik+qwm+qws8H+XkkfL0VQuvHhGuR1iAz7iJHhIoGIlPHcitwijMmkDCjDwtP1itzQGJxNAoGfEtignwlTTYDDFQ0NtU0gB5oCh9xcNNEhS6l0JIGBG0NjRFQVynAkMNGwGVusvrFMpVoyA9HVpzFhuMjqqwAdsonuOhkfPlgdzhvtC2pcOvHBF0ISrQBVC1qyF1ufGIh+NQkOLeAanzhCWQFkkTxtOVmgmLEej4F1DWm4EKpJGoHwpzukHbULuOMLNQhcKtr5oRNjs7vZqPPAlbE+PHwtw9DrzLPAlBoDErwfChmx2uJVz+Mxv1AAjgmPEWpVrTNNA5qxVFDmiMJPJZQwlQFqmqGmAU2TRmI9KgvDw9TDQJODGyDYFLX9NYuxKktUKhLJOJOHKyqRj2qUAnPHwKqyLb2KrnKZLKqgAb0vMRJ9XVHFGBrb1AbyvAeqEnwxnpNn1gfbuDVRqnVTmhoavjryEikZpHC4EpqKUQCvXyUZSghHCyl0D0HKlIgQkYY0EZUXs5BIEtgxt7Nr3FlgUqvQOquwLRr+HDvIYmsduBD+DTtPDat+1aJoOPO6u6KYNlMvoVN7N1LAtUpASB12i/IHOjOw0WNSgwD1MUD/qjT2tArdSkprAuNWI0r128oDFZutIEkZzFyeONqEXRUAhiLvpUtDFQlfGyQYPYBsR9u9DDl3MMo/MpB3qtN+MCdQxgSEFNCgpIqMqdrtBcPcTDHlhgRUGtJksgAQdWNsuJHsoRNhkeoeOIajXm1QZV2ZrzLdXzp8rnp/Whp3BNRcqPmyzGdcreKttcWTQFwsucW7p8sPQ5EzN2J+HbXisSuRn1hUe1tDtgGNhYWuXvOhslw9j2dZtPAFO4CVoFIyAoDOeFqEZ7JSv7IYvAldGUtJiMAyqExHOypAbSPyuwZpNwegeU2ZPKqsfJLhkwZhIgdpKgdnKhYtsabhPuokZuAyjw28k0AIE5gPI0aPK6q6KlNhb6P1lxYPHxY0EJYcxMVh0+SVq1Bhn1NL3ggXgYHkBWphMrzeCIEwHgBA2zvfpuw9FU3bUbgqiwD5etAfFEEcfsnmXoAdxYDsNbDpUgHBEYhYjYkbBhjpF4lNXWHFh1YAD09XWIEhwQU8jQ/Uel5WsApFqcNhyIqA511X/ZwQLWbWDWkdfWmI2IAA2DQXwO1oh/WVqMlfWYiaNocLcuNlOJwXmn6wIDE8RVDfWOIPYXaEKS2dw94Twrkb1f9Udb18XAAbQIEUnIPbk7g0CaxUSBPCkQAAF1LZjUo1INrxkqU4a3yl62O5ZoJW23O3bUj5maJLuhDE9TPsA9/YsIhRJNlFuWLbt4nbKhqhmZdSU9U2SB+6M2ZGLnrYY2wyD7qLS3k1LdjzK3fXfANAkgA2upfX6IAB2ZicNlUehEKOPOdnFdCUeSgVah1uILFNe8vQuH1bGqNxpJNkMDwwBKRYOLa8Qxa51y1vV79lPQduttGEdz8Zt9d6QCd7t+yJS7VNEAagDldBxdQHBJ8Yt8xDFtgbPegXt4Mftsdi28jyM89ehUcYsppjuKp1yHnE/LIF1Ggf2aydUWyKNYR7MUR82cRv5kkwF4BpB+FsfRFsFput1SF+chfOp32vtJHHBmchk3GUIyIzytvNVISHT9I1j+EgAbkTs5Zbdu0vfjT/eVeyodpPRSvURpa8i6HpZYEZdY4ceQvEe5iqoMAelqJqszdcAex3LhUmOFI6pRDJeFgpd0fPTafwRk5tRcNTkzRICAxJhmqfyvxyoQSVvEDYGMz9XRknGZzsQxJTjRkdHCRID4LYHVn2CvTfDmcG/9IeZA+NjSmJxjidormTZNIro1Aw2E8YGFCwmPBa4QTQQ2Dq5UjrwclQtNFQk1tRF240H28iiyg0B429hIEilwCA0NDLgAF5muqv6WquavWS8AGAsp2cYYLuaArubvdu/vSYAegf7VUB9SLaKothrxUhoApRAxfuiFphVvsw9rY08ybUvv37BB8XiJEAFih4TuNXzqqxTpPZvYOLIeiFQwbESEWnUktrBBJDP5fGHUtubVduHlUf0emflC0BqRtvpBJUNOgG4WMHdPsqkWIXUXjPihTPYXGvcwFtgba7nPQWUYDOcjrO0Xa0y3mm2TEvkvOS0u5HDn3cFulHeCXqyCmj2qSXOq6CCvGDzyDAWktg2Cre1ybeyG7fFGK4nf+CXfhC5Y4gxCGv9RufCp5DnliQinFq6R4cw4pJNDwIBddDzvDDjDTC4eLDKRHmbCn8sA/Yvil4HD3InD/agkmPPCoMNI0lmbxQGSBIZXrRtQNJB0CpRWhktc0lbDn9m/SAAHMr0jgWwG9ODfIHDOVeCG1NFzfmEvJGkvv4UvpHslZGQ/Gq4b7fi9w+Jp5uqBVHXf1GEQ2i8vPfJTveZTz0aOmWJCbUlO9FVS6eVPSAg1z19GjGMxKwGIC5AIBho7hLvB7HCqFRaA+TEcG7X7hf07Gv9A0pOWcYt4GAGZA8FmT2Jt5O6BZPyGcXbQRIW05/dOrc3uZ20nm4dftHjXAgvUYIIrBZFgENBgBhuWkWZnhGrjuRZ4HyNCi2V2ojMFmGGFwnSCoHERb29ya8DQMgQEV4k+zCgaiGm7UDoaWbFdInkoAlImws4NSOCkWbdRDEL6VMl4S8RYBJBV8AnmIFkJWVS0LjPgBEiiKaCYSGLbvN6jU7/NSS4+Wfgi0V76dF+RvfBqVQXIVUN+NwQPqlxkbpcD+FDZqmf1apX8WiN/TRvlwf5nkn+wYCJGtHfzJ0ECtvQ/nkHDJTopE5gtlvT3mroAHEu6T6Ls2sSvULAIZQdOfyBrrRPci1KoV6GxBXMV6ysBoQcjDKyVqAlPToQoMObHMDoQYZ5mQFPYpsvqP1WbrQH+paBrkGgUCGsjvyAlJhT5SDjXinpiNiBeCLstWViTnwgyyMVgewPwicCKcxEdwImxMpoBG6IKahjojWYs0YiXURwTSkV7o1pAaMARqV33qbR2abARPCW27LJUzQHzQckQyMYVlwKiGXZDmH+qrNLBPbLrl3g0FfDm4Xpe3G6mvCGgFagJIWt4gmgIY8QjPfqHcjRAgjsggCB3AAhEGoUDCRhQkfQD7o/VfKgtaANDQ84aYAA6tABgAjNPAiebAKYSRzMjdw1YNkbMP5pcieRzmAUUKNjoii1oJAYvurzhEa9tQCfbARsy2AHE+mXdQgahFc5Yt1OgDGftp3l5Uk/ByLJfsb1V7lUlyoQq4OMB3DbtZgyQq+CsDWCbBT6e0AiIMFOAjALghgd0b4mIL4oGqe4VpHQHm55swx1wKAL4AEAMBfA9EENgwASBMQAAnAAA4BAtAAAKz5iHACQEgLmPLG+AHAIbXMcWKYgNiSASQBIA4F8DvsGAVY3wMSDGBlBIA+Yp2o2Poj0QHA9EWgAIBDZoAQ2mYkNu+18AJB32KgAsbmLQBJBmx9EBgExE3EKx32G4pMe6PErNiSAvgBgO+yYi+AQ2SgYccWM3HvtD277Kse+wEBnj32SQWgLmIVgOA7x+YlQPuL7G0BWo9EJiC2M7H0R8xxYusQIAXEMAkgDgJICGwbEhskgaAfMY2NPEMB8xIbAQPRDQDzikxBgCMWGijEMMsIsYzxDwgqBhigAA== -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=santoshgdev/fitnessllm-dataplatform&utm_content=24):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov-commenter
created_at: 2025-05-09T02:20:24Z
body: ## Welcome to [Codecov](https://codecov.io?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=santoshgdev) :tada:

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

:information_source: You can also turn on [project coverage checks](https://docs.codecov.com/docs/common-recipe-list#set-project-coverage-checks-on-a-pull-request) and [project coverage reporting on Pull Request comment](https://docs.codecov.com/docs/common-recipe-list#show-project-coverage-changes-on-the-pull-request-comment)

Thanks for integrating Codecov - We've got you covered :open_umbrella:
author_association: NONE