Skip to content

Commit 5acb371

Browse files
mikejhillCopilot
andcommitted
refactor: update to Python skill standards and fix CI
- Switch type checker from mypy to ty - Expand ruff rules: add D, ANN, PT, RET, ARG, FA, N, RUF rule sets - Add from __future__ import annotations to all modules - Add Google-style pydocstyle convention - Fix all ruff violations (docstrings, assertions, type alias syntax) - Update requires-python from >=3.10 to >=3.12 - Configure ty to check api_client (HA modules need homeassistant stubs) CI fixes: - Update actions/checkout v4 -> v6 (Node.js 24) - Update astral-sh/setup-uv v6 -> v7 - Update amannn/action-semantic-pull-request v5 -> v6 - Update DavidAnson/markdownlint-cli2-action v20 -> v23 - Fix lint job: mypy -> ty check - Fix coverage path: --cov=src -> --cov=custom_components Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a6d7542 commit 5acb371

24 files changed

+153
-592
lines changed

.github/workflows/ci.yml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,21 @@ jobs:
2323
runs-on: ubuntu-latest
2424
timeout-minutes: 5
2525
steps:
26-
- uses: amannn/action-semantic-pull-request@v5
26+
- uses: amannn/action-semantic-pull-request@v6
2727
env:
2828
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2929

3030
lint:
3131
runs-on: ubuntu-latest
3232
timeout-minutes: 10
3333
steps:
34-
- uses: actions/checkout@v4
35-
- uses: astral-sh/setup-uv@v6
34+
- uses: actions/checkout@v6
35+
- uses: astral-sh/setup-uv@v7
3636
- run: uv sync --group dev
3737
- run: uv run ruff check .
3838
- run: uv run ruff format --check .
39-
- name: Type check (core library only)
40-
run: uv run mypy src --ignore-missing-imports
39+
- name: Type check
40+
run: uv run ty check
4141

4242
test:
4343
runs-on: ${{ matrix.os }}
@@ -53,15 +53,15 @@ jobs:
5353
coverage: true
5454
name: "Test (${{ matrix.os }}, Python ${{ matrix.python-version }})"
5555
steps:
56-
- uses: actions/checkout@v4
57-
- uses: astral-sh/setup-uv@v6
56+
- uses: actions/checkout@v6
57+
- uses: astral-sh/setup-uv@v7
5858
- run: uv python install ${{ matrix.python-version }}
5959
- run: uv sync --group dev --python ${{ matrix.python-version }}
6060

6161
- name: Run tests
6262
run: >-
6363
uv run pytest --junit-xml=test-results.xml
64-
${{ matrix.coverage && '--cov=src --cov-report=xml --cov-fail-under=80' || '' }}
64+
${{ matrix.coverage && '--cov=custom_components --cov-report=xml --cov-fail-under=80' || '' }}
6565
6666
- name: Publish test report
6767
if: always()
@@ -81,8 +81,8 @@ jobs:
8181
runs-on: ubuntu-latest
8282
timeout-minutes: 10
8383
steps:
84-
- uses: actions/checkout@v4
85-
- uses: astral-sh/setup-uv@v6
84+
- uses: actions/checkout@v6
85+
- uses: astral-sh/setup-uv@v7
8686
- run: uv build
8787
- uses: actions/upload-artifact@v4
8888
with:
@@ -94,8 +94,8 @@ jobs:
9494
runs-on: ubuntu-latest
9595
timeout-minutes: 5
9696
steps:
97-
- uses: actions/checkout@v4
98-
- uses: DavidAnson/markdownlint-cli2-action@v20
97+
- uses: actions/checkout@v6
98+
- uses: DavidAnson/markdownlint-cli2-action@v23
9999

100100
ci-pass:
101101
if: always()

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
timeout-minutes: 30
2222
steps:
2323
- name: Checkout
24-
uses: actions/checkout@v4
24+
uses: actions/checkout@v6
2525
with:
2626
fetch-depth: 0
2727

@@ -41,11 +41,11 @@ jobs:
4141
exit 1
4242
4343
# ── Validation ──────────────────────────────────────────
44-
- uses: astral-sh/setup-uv@v6
44+
- uses: astral-sh/setup-uv@v7
4545
- run: uv sync --group dev
4646
- run: uv run ruff check .
4747
- run: uv run ruff format --check .
48-
- run: uv run mypy src --ignore-missing-imports
48+
- run: uv run ty check
4949
- run: uv run pytest
5050

5151
# ── CHANGELOG ───────────────────────────────────────────

custom_components/rouvy/__init__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ async def async_setup_entry(
3838
)
3939

4040
await coordinator.async_config_entry_first_refresh()
41-
await hass.config_entries.async_forward_entry_setups(
42-
entry, [Platform.SENSOR]
43-
)
41+
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
4442

4543
_register_services(hass)
4644

@@ -54,9 +52,7 @@ async def async_unload_entry(
5452
"""Unload a Rouvy config entry."""
5553
from homeassistant.const import Platform
5654

57-
result: bool = await hass.config_entries.async_unload_platforms(
58-
entry, [Platform.SENSOR]
59-
)
55+
result: bool = await hass.config_entries.async_unload_platforms(entry, [Platform.SENSOR])
6056
return result
6157

6258

custom_components/rouvy/api_client/__main__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#!/usr/bin/env python3
2-
"""
3-
Rouvy API client - Command-line interface for making API calls.
2+
"""Rouvy API client - Command-line interface for making API calls.
43
54
Supports both subcommands (profile, zones, apps, activities, set, raw)
65
and legacy flags (--endpoint, --set, --raw) for backward compatibility.
@@ -528,6 +527,7 @@ def _legacy_main(client: RouvyClient, args: argparse.Namespace) -> None:
528527

529528

530529
def main() -> None:
530+
"""Parse arguments, configure logging, and dispatch the CLI command."""
531531
args = _parse_args()
532532
log_level = "DEBUG" if args.debug else args.log_level
533533
_configure_logging(log_level)

custom_components/rouvy/api_client/models.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""
2-
Typed data models for Rouvy API responses.
1+
"""Typed data models for Rouvy API responses.
32
43
These frozen dataclasses represent the structured data returned by various
54
Rouvy API endpoints. They are HTTP-library-agnostic and can be used by

custom_components/rouvy/api_client/parser.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""
2-
Parser for Rouvy API responses using turbo-stream format.
1+
"""Parser for Rouvy API responses using turbo-stream format.
32
43
Rouvy uses the turbo-stream format from Remix (https://github.com/jacob-ebey/turbo-stream),
54
which is a streaming data format that supports more types than JSON and uses indexed
@@ -40,8 +39,7 @@ def __init__(self) -> None:
4039
self.promise_values: dict[int, Any] = {}
4140

4241
def decode(self, response_text: str) -> dict[str, Any] | list[Any] | Any:
43-
"""
44-
Decode a turbo-stream formatted response.
42+
"""Decode a turbo-stream formatted response.
4543
4644
Args:
4745
response_text: Raw response text (may be multi-line)
@@ -89,8 +87,7 @@ def _parse_promise_line(self, line: str) -> None:
8987
LOGGER.warning(f"Error parsing promise line '{line}': {e}")
9088

9189
def _decode_value(self, value: Any, resolve_int_as_index: bool = False) -> Any:
92-
"""
93-
Recursively decode a value, resolving references and special types.
90+
"""Recursively decode a value, resolving references and special types.
9491
9592
Args:
9693
value: The value to decode
@@ -100,10 +97,10 @@ def _decode_value(self, value: Any, resolve_int_as_index: bool = False) -> Any:
10097
if isinstance(value, int):
10198
if value == -5:
10299
return UNDEFINED
103-
elif value == -7:
100+
if value == -7:
104101
return NULL
105102
# Only resolve as index if explicitly requested (for indexed object values)
106-
elif (
103+
if (
107104
resolve_int_as_index
108105
and value in self.index_map
109106
and value not in getattr(self, "_resolving", set())
@@ -136,9 +133,8 @@ def _decode_value(self, value: Any, resolve_int_as_index: bool = False) -> Any:
136133
return self._decode_value(
137134
self.promise_values[promise_id], resolve_int_as_index=False
138135
)
139-
else:
140-
# Promise not yet resolved
141-
return f"<Promise:{promise_id}>"
136+
# Promise not yet resolved
137+
return f"<Promise:{promise_id}>"
142138

143139
# Regular array - decode each element (don't resolve ints as indices)
144140
return [self._decode_value(item, resolve_int_as_index=False) for item in value]
@@ -183,8 +179,7 @@ def _decode_value(self, value: Any, resolve_int_as_index: bool = False) -> Any:
183179
return value
184180

185181
def extract_data_section(self, decoded: Any, path: str = "root.data") -> dict[str, Any]:
186-
"""
187-
Extract a specific data section from the decoded structure.
182+
"""Extract a specific data section from the decoded structure.
188183
189184
Args:
190185
decoded: Decoded turbo-stream data
@@ -210,8 +205,7 @@ def extract_data_section(self, decoded: Any, path: str = "root.data") -> dict[st
210205

211206

212207
def parse_response(response_text: str) -> dict[str, Any] | list[Any] | Any:
213-
"""
214-
Parse a Rouvy API response in turbo-stream format.
208+
"""Parse a Rouvy API response in turbo-stream format.
215209
216210
Args:
217211
response_text: Raw response text
@@ -224,8 +218,7 @@ def parse_response(response_text: str) -> dict[str, Any] | list[Any] | Any:
224218

225219

226220
def extract_user_profile(response_text: str) -> dict[str, Any]:
227-
"""
228-
Extract user profile fields from user-settings response.
221+
"""Extract user profile fields from user-settings response.
229222
230223
Args:
231224
response_text: Raw user-settings.data response

custom_components/rouvy/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Constants for the Rouvy integration."""
22

3+
from __future__ import annotations
4+
35
import logging
46

57
DOMAIN = "rouvy"

custom_components/rouvy/data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6-
from typing import TYPE_CHECKING, TypeAlias
6+
from typing import TYPE_CHECKING
77

88
from homeassistant.config_entries import ConfigEntry
99
from homeassistant.loader import Integration
@@ -22,4 +22,4 @@ class RouvyData:
2222
integration: Integration
2323

2424

25-
RouvyConfigEntry: TypeAlias = ConfigEntry[RouvyData]
25+
type RouvyConfigEntry = ConfigEntry[RouvyData]

pyproject.toml

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "rouvy-api"
77
version = "0.1.0"
88
description = "A Home Assistant integration and CLI for the Rouvy indoor cycling platform with turbo-stream API support"
99
readme = "README.md"
10-
requires-python = ">=3.10"
10+
requires-python = ">=3.12"
1111
authors = [{ name = "Mike Hill" }]
1212
dependencies = ["requests>=2.31.0", "python-dotenv>=1.0.0"]
1313

@@ -16,13 +16,13 @@ rouvy-api = "custom_components.rouvy.api_client.__main__:main"
1616

1717
[dependency-groups]
1818
dev = [
19-
"pytest>=8.0.0",
20-
"pytest-cov>=6.0.0",
19+
"pytest>=8.0",
20+
"pytest-cov>=6.0",
2121
"pytest-asyncio>=0.24.0",
2222
"responses>=0.25.0",
2323
"aiohttp>=3.9.0",
24-
"ruff>=0.9.0",
25-
"mypy>=1.14.0",
24+
"ruff>=0.7",
25+
"ty>=0.0.1",
2626
"types-requests>=2.31.0",
2727
]
2828

@@ -31,22 +31,39 @@ packages = ["custom_components"]
3131

3232
[tool.ruff]
3333
line-length = 100
34-
target-version = "py310"
34+
target-version = "py312"
35+
src = ["custom_components", "tests"]
3536

3637
[tool.ruff.lint]
37-
select = ["E", "F", "I", "W", "UP", "B", "SIM"]
38+
select = ["E", "W", "F", "I", "N", "UP", "B", "SIM", "RUF", "D", "ANN", "PT", "RET", "ARG", "FA"]
39+
ignore = ["D100", "D104", "D107", "D203", "D213"]
40+
41+
[tool.ruff.lint.pydocstyle]
42+
convention = "google"
43+
44+
[tool.ruff.lint.isort]
45+
known-first-party = ["custom_components"]
3846

3947
[tool.ruff.lint.per-file-ignores]
40-
"tests/test_cli.py" = ["E402"]
41-
"tests/test_ha_api.py" = ["E402"]
42-
"scripts/*" = ["E501"]
43-
44-
[tool.mypy]
45-
python_version = "3.10"
46-
warn_return_any = true
47-
warn_unused_configs = true
48-
disallow_untyped_defs = true
49-
check_untyped_defs = true
48+
"tests/*" = ["D101", "D102", "D103", "ANN", "ARG", "E402", "PT017"]
49+
"scripts/*" = ["D102", "D103", "ANN", "E501"]
50+
"custom_components/rouvy/__init__.py" = ["ANN401"]
51+
"custom_components/rouvy/coordinator.py" = ["ANN401"]
52+
"custom_components/rouvy/sensor.py" = ["ANN401", "ARG001"]
53+
"custom_components/rouvy/config_flow.py" = ["ANN401"]
54+
"custom_components/rouvy/entity.py" = ["ANN401"]
55+
"custom_components/rouvy/api.py" = ["ANN401"]
56+
"custom_components/rouvy/api_client/client.py" = ["ANN401"]
57+
"custom_components/rouvy/api_client/errors.py" = ["ANN401"]
58+
"custom_components/rouvy/api_client/parser.py" = ["ANN401"]
59+
60+
[tool.ty.environment]
61+
python-version = "3.12"
62+
63+
[tool.ty.src]
64+
# Only type-check the standalone API client; HA integration modules and tests
65+
# require homeassistant stubs which are not available in the dev environment.
66+
include = ["custom_components/rouvy/api_client/"]
5067

5168
[tool.pytest.ini_options]
5269
testpaths = ["tests"]

scripts/debug_parser.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/env python3
22
"""Debug script to understand turbo-stream structure."""
33

4+
from __future__ import annotations
5+
46
import json
57
import os
68
import sys
@@ -37,7 +39,7 @@
3739
end = min(len(raw_data), i + 10)
3840
for j in range(start, end):
3941
marker = " <-- HERE" if j == i else ""
40-
print(f" [{j}]: {repr(raw_data[j])}{marker}")
42+
print(f" [{j}]: {raw_data[j]!r}{marker}")
4143
break
4244

4345
# Find userProfile section
@@ -48,7 +50,7 @@
4850
end = min(len(raw_data), i + 5)
4951
for j in range(start, end):
5052
marker = " <-- HERE" if j == i else ""
51-
print(f" [{j}]: {repr(raw_data[j])}{marker}")
53+
print(f" [{j}]: {raw_data[j]!r}{marker}")
5254

5355
# The value should be an object
5456
if i + 1 < len(raw_data):
@@ -66,4 +68,4 @@
6668
# Show just the first 20 elements decoded
6769
if isinstance(decoded, list):
6870
for i in range(min(20, len(decoded))):
69-
print(f"[{i}]: {repr(decoded[i])}")
71+
print(f"[{i}]: {decoded[i]!r}")

0 commit comments

Comments
 (0)