From 6eee7a3d8c441b87e376b995d4ad3c2329a51067 Mon Sep 17 00:00:00 2001 From: Jon Stroop Date: Fri, 28 Feb 2025 06:34:32 -0500 Subject: [PATCH 1/2] add warn_unreachable to mypy --- TODO.md | 14 ++++++++++++++ fitbit_client/resources/nutrition.py | 9 ++++----- pyproject.toml | 13 +++---------- tests/resources/nutrition/test_create_food.py | 8 ++++---- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/TODO.md b/TODO.md index adca8fc..df86bf9 100644 --- a/TODO.md +++ b/TODO.md @@ -33,6 +33,20 @@ if not food_id and not (food_name and calories): ) ``` +- nutrition.py: +- It doesn't seem like this should be passing tests when CALORIES is not an int: + +```python + # Handle both enum and string nutritional values + for key, value in nutritional_values.items(): + if isinstance(key, NutritionalValue): + params[key.value] = float(value) + else: + params[str(key)] = float(value) +``` + +see: test_create_food_calories_from_fat_must_be_integer(nutrition_resource) + - exceptions.py - Should ClientValidationException really subclass FitbitAPIException? It diff --git a/fitbit_client/resources/nutrition.py b/fitbit_client/resources/nutrition.py index 0f8cbba..3999d28 100644 --- a/fitbit_client/resources/nutrition.py +++ b/fitbit_client/resources/nutrition.py @@ -61,7 +61,7 @@ def create_food( calories: int, description: str, form_type: FoodFormType, - nutritional_values: Dict[NutritionalValue, float], + nutritional_values: Dict[NutritionalValue | str, float | int], user_id: str = "-", debug: bool = False, ) -> JSONDict: @@ -98,11 +98,10 @@ def create_food( and NutritionalValue.CALORIES_FROM_FAT in nutritional_values and not isinstance(nutritional_values[NutritionalValue.CALORIES_FROM_FAT], int) ): - raise ValidationException( + raise ClientValidationException( message="Calories from fat must be an integer", - status_code=400, - error_type="validation", - field_name="caloriesFromFat", + error_type="client_validation", + field_name="CALORIES_FROM_FAT", ) # Handle both enum and string nutritional values for key, value in nutritional_values.items(): diff --git a/pyproject.toml b/pyproject.toml index 94734c3..6f0574f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,27 +110,20 @@ strict_optional = true warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true -# warn_unreachable = true +warn_unreachable = true # disallow_any_generics = true disallow_subclassing_any = true disallow_untyped_calls = true - -# Show details about error locations -# show_column_numbers = true +# output formatting show_error_codes = true pretty = true error_summary = true show_error_context = true -# show_traceback = true - -# Necessary for most libraries [[tool.mypy.overrides]] module = [ "requests.*", "requests_oauthlib.*", - "pytest.*", - "pyOpenSSL.*" ] ignore_missing_imports = true @@ -138,7 +131,7 @@ ignore_missing_imports = true [tool.pdm.scripts] autoflake = { cmd = "autoflake . -r --remove-unused-variables --remove-all-unused-imports --ignore-pass-after-docstring --exclude ./.venv/*,./_scripts/*" } headers = { cmd = "python lint/add_file_headers.py" } -typecheck = { cmd = "mypy --show-error-codes --pretty --no-incremental fitbit_client" } +mypy = { cmd = "mypy --pretty --no-incremental --warn-unused-configs fitbit_client" } black = { cmd = "black ." } isort = { cmd = "isort ." } mdformat = { cmd = "mdformat ." } diff --git a/tests/resources/nutrition/test_create_food.py b/tests/resources/nutrition/test_create_food.py index 4d98629..8bc1c79 100644 --- a/tests/resources/nutrition/test_create_food.py +++ b/tests/resources/nutrition/test_create_food.py @@ -6,6 +6,7 @@ from pytest import raises # Local imports +from fitbit_client.exceptions import ClientValidationException from fitbit_client.exceptions import ValidationException from fitbit_client.resources.constants import FoodFormType from fitbit_client.resources.constants import NutritionalValue @@ -82,7 +83,7 @@ def test_create_food_with_string_nutritional_values(nutrition_resource, mock_res def test_create_food_calories_from_fat_must_be_integer(nutrition_resource): """Test that calories_from_fat must be an integer""" - with raises(ValidationException) as exc_info: + with raises(ClientValidationException) as exc_info: nutrition_resource.create_food( name="Test Food", default_food_measurement_unit_id=147, @@ -98,7 +99,6 @@ def test_create_food_calories_from_fat_must_be_integer(nutrition_resource): ) # Float instead of integer # Verify exception details - assert exc_info.value.status_code == 400 - assert exc_info.value.error_type == "validation" - assert exc_info.value.field_name == "caloriesFromFat" + assert exc_info.value.error_type == "client_validation" + assert exc_info.value.field_name == "CALORIES_FROM_FAT" assert "Calories from fat must be an integer" in str(exc_info.value) From fceef59c83c147386344a593b5dce979a9e601fc Mon Sep 17 00:00:00 2001 From: Jon Stroop Date: Fri, 28 Feb 2025 23:09:51 -0500 Subject: [PATCH 2/2] ci setup --- .github/workflows/ci.yml | 152 +++++++++++++++++++++++++++++++++++++++ README.md | 9 ++- codecov.yml | 44 ++++++++++++ pyproject.toml | 1 + 4 files changed, 203 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 codecov.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9963569 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,152 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python 3.13 + uses: actions/setup-python@v4 + with: + python-version: "3.13" + + - name: Install PDM + run: | + python -m pip install --upgrade pip + pip install pdm + + - name: Install dependencies + run: | + pdm install -G:all + + - name: Run tests with coverage + run: | + pdm run pytest --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + fail_ci_if_error: false + + format: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python 3.13 + uses: actions/setup-python@v4 + with: + python-version: "3.13" + + - name: Install PDM + run: | + python -m pip install --upgrade pip + pip install pdm + + - name: Install dependencies + run: | + pdm install -G:all + + - name: Check Black formatting + run: | + pdm run black --check + + - name: Check isort + run: | + pdm run isort --check + + - name: Check unused imports with autoflake + run: | + pdm run autoflake + + type-check: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python 3.13 + uses: actions/setup-python@v4 + with: + python-version: "3.13" + + - name: Install PDM + run: | + python -m pip install --upgrade pip + pip install pdm + + - name: Install dependencies + run: | + pdm install -G:all + + - name: Run mypy + run: | + pdm run mypy + + docs: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v3 + + - name: Set up Python 3.13 + uses: actions/setup-python@v4 + with: + python-version: "3.13" + + - name: Install PDM + run: | + python -m pip install --upgrade pip + pip install pdm + + - name: Install dependencies + run: | + pdm install -G:all + + - name: Check markdown formatting + run: | + echo "Markdown format checking temporarily disabled" + exit 0 + + # todo: https://github.com/ydah/mdformat-action + + # build: + # runs-on: ubuntu-latest + # needs: [test, format, type-check, docs] + + # steps: + # - name: Check out repository + # uses: actions/checkout@v3 + + # - name: Set up Python 3.13 + # uses: actions/setup-python@v4 + # with: + # python-version: "3.13" + + # - name: Install PDM + # run: | + # python -m pip install --upgrade pip + # pip install pdm + + # - name: Install dependencies + # run: | + # pdm install -G:all + + # - name: Build package + # run: | + # pdm build \ No newline at end of file diff --git a/README.md b/README.md index 6c5ad3b..ae7c895 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # Python API Client for Fitbit™ -[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) -[![PDM](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev) +# Fitbit Client + +[![CI](https://github.com/jpstroop/fitbit-client/actions/workflows/ci.yml/badge.svg)](https://github.com/jpstroop/fitbit-client/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/jpstroop/fitbit-client/branch/main/graph/badge.svg)](https://codecov.io/gh/jpstroop/fitbit-client) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL%203.0-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) +[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/release/python-3130/) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) A fully-typed Python client for interacting with the Fitbit API, featuring OAuth2 PKCE authentication and resource-based API interactions. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..c1ca2f7 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,44 @@ +codecov: + require_ci_to_pass: yes + strict_yaml_branch: main + +coverage: + precision: 2 + round: down + range: "95...100" + status: + project: + default: + target: 100% + threshold: 0% + base: auto + branches: + - main + if_not_found: failure + if_ci_failed: error + informational: false + only_pulls: false + patch: + default: + target: 100% + threshold: 0% + base: auto + branches: + - main + if_not_found: failure + if_ci_failed: error + informational: false + only_pulls: false + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "reach,diff,flags,files,footer" + behavior: default + require_changes: no \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6f0574f..79fc1de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ codeformatters = [ extensions = [ "gfm" ] +exclude = [".venv/**"] [tool.pytest.ini_options] testpaths = ["tests"]