-
Notifications
You must be signed in to change notification settings - Fork 34
Add action groups to diagnostics #2016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+161
−21
Merged
Changes from 6 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
e907047
Add design spec for diagnostics action groups support
iMicknl ead8bc1
Add implementation plan for diagnostics action groups
iMicknl d243a43
Support list input in obfuscate_sensitive_data
iMicknl 747522d
Remove unused JSON import from obfuscate module
iMicknl 2b5fc78
Return structured dict from get_diagnostic_data with action groups
iMicknl 7cf2e5c
Document get_diagnostic_data breaking change in migration guide
iMicknl 9040cf6
Fix linting, type errors, use asyncio.gather, and improve test assert…
Copilot f0be21d
Remove redundant isinstance assertion in diagnostic test
Copilot 160bf66
Remove internal planning docs from repository
iMicknl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,320 @@ | ||
| # Diagnostics: Add Action Groups Implementation Plan | ||
|
|
||
| > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. | ||
|
|
||
| **Goal:** Expand `get_diagnostic_data()` to return a structured dict containing both setup and action group data, with per-section obfuscation. | ||
|
|
||
| **Architecture:** The method fetches two API endpoints (`setup` and `actionGroups`) concurrently, wraps them in a `{"setup": ..., "action_groups": [...]}` dict, and applies `obfuscate_sensitive_data` to each section individually. The obfuscation function is extended to accept lists of dicts in addition to plain dicts. | ||
|
|
||
| **Tech Stack:** Python 3.12+, aiohttp, pytest, pytest-asyncio | ||
|
|
||
| --- | ||
|
|
||
| ### Task 1: Extend `obfuscate_sensitive_data` to accept lists | ||
|
|
||
| **Files:** | ||
| - Modify: `pyoverkiz/obfuscate.py:27-68` | ||
| - Test: `tests/test_obfuscate.py` | ||
|
|
||
| - [ ] **Step 1: Write failing test for list input** | ||
|
|
||
| Add to `tests/test_obfuscate.py`: | ||
|
|
||
| ```python | ||
| def test_obfuscate_list_of_dicts(self): | ||
| """Ensure obfuscate_sensitive_data handles a list of dicts.""" | ||
| data = [ | ||
| {"label": "My Scene", "oid": "abc-123"}, | ||
| {"label": "Night Mode", "deviceURL": "io://1234-5678-1234/12345678"}, | ||
| ] | ||
| result = obfuscate_sensitive_data(data) | ||
| assert isinstance(result, list) | ||
| assert len(result) == 2 | ||
| assert result[0]["label"] != "My Scene" | ||
| assert result[0]["oid"] == "abc-123" # oid is not a sensitive key | ||
| assert result[1]["label"] != "Night Mode" | ||
| assert result[1]["deviceURL"] != "io://1234-5678-1234/12345678" | ||
| ``` | ||
|
|
||
| - [ ] **Step 2: Run test to verify it fails** | ||
|
|
||
| Run: `pytest tests/test_obfuscate.py::TestObfucscateSensitive::test_obfuscate_list_of_dicts -v` | ||
| Expected: FAIL — `TypeError` because `obfuscate_sensitive_data` calls `.items()` on a list. | ||
|
|
||
| - [ ] **Step 3: Implement list support in `obfuscate_sensitive_data`** | ||
|
|
||
| In `pyoverkiz/obfuscate.py`, change the function signature and add a list guard at the top: | ||
|
|
||
| ```python | ||
| def obfuscate_sensitive_data( | ||
| data: dict[str, Any] | list[dict[str, Any]], | ||
| ) -> dict[str, Any] | list[dict[str, Any]]: | ||
| """Mask Overkiz JSON data to remove sensitive data.""" | ||
| if isinstance(data, list): | ||
| return [obfuscate_sensitive_data(item) for item in data] | ||
|
|
||
| mask_next_value = False | ||
| # ... rest unchanged ... | ||
| ``` | ||
|
|
||
| - [ ] **Step 4: Run test to verify it passes** | ||
|
|
||
| Run: `pytest tests/test_obfuscate.py -v` | ||
| Expected: All tests PASS. | ||
|
|
||
| - [ ] **Step 5: Commit** | ||
|
|
||
| ```bash | ||
| git add pyoverkiz/obfuscate.py tests/test_obfuscate.py | ||
| git commit -m "Support list input in obfuscate_sensitive_data" | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### Task 2: Update `get_diagnostic_data()` to return structured dict | ||
|
|
||
| **Files:** | ||
| - Modify: `pyoverkiz/client.py:332-348` | ||
| - Test: `tests/test_client.py` | ||
|
|
||
| - [ ] **Step 1: Write failing test for new return shape** | ||
|
|
||
| Add to `tests/test_client.py` in the `TestOverkizClient` class. This test verifies the new return shape contains both `setup` and `action_groups` keys: | ||
|
|
||
| ```python | ||
| @pytest.mark.asyncio | ||
| async def test_get_diagnostic_data_returns_structured_dict(self, client: OverkizClient): | ||
| """Verify diagnostic data returns a dict with setup and action_groups sections.""" | ||
| with (CURRENT_DIR / "fixtures" / "setup" / "setup_tahoma_1.json").open( | ||
| encoding="utf-8", | ||
| ) as setup_mock: | ||
| setup_resp = MockResponse(setup_mock.read()) | ||
|
|
||
| with (CURRENT_DIR / "fixtures" / "action_groups" / "action-group-tahoma-switch.json").open( | ||
| encoding="utf-8", | ||
| ) as ag_mock: | ||
| ag_resp = MockResponse(ag_mock.read()) | ||
|
|
||
| responses = iter([setup_resp, ag_resp]) | ||
|
|
||
| with patch.object(aiohttp.ClientSession, "get", side_effect=lambda *a, **kw: next(responses)): | ||
| diagnostics = await client.get_diagnostic_data(mask_sensitive_data=False) | ||
|
|
||
| assert "setup" in diagnostics | ||
| assert "action_groups" in diagnostics | ||
| assert isinstance(diagnostics["action_groups"], list) | ||
| ``` | ||
|
|
||
| - [ ] **Step 2: Run test to verify it fails** | ||
|
|
||
| Run: `pytest tests/test_client.py::TestOverkizClient::test_get_diagnostic_data_returns_structured_dict -v` | ||
| Expected: FAIL — current implementation returns flat setup dict without `action_groups` key. | ||
|
|
||
| - [ ] **Step 3: Implement the new `get_diagnostic_data`** | ||
|
|
||
| Replace the method body in `pyoverkiz/client.py`: | ||
|
|
||
| ```python | ||
| @retry_on_auth_error | ||
| async def get_diagnostic_data(self, mask_sensitive_data: bool = True) -> dict[str, Any]: | ||
| """Get diagnostic data for the connected user setup. | ||
|
|
||
| -> gateways data (serial number, activation state, ...): <gateways/gateway> | ||
| -> setup location: <location> | ||
| -> house places (rooms and floors): <place> | ||
| -> setup devices: <devices> | ||
| -> action groups: <actionGroups> | ||
|
|
||
| By default, this data is masked to not return confidential or PII data. | ||
| Set `mask_sensitive_data` to `False` to return the raw payloads. | ||
| """ | ||
| setup = await self._get("setup") | ||
| action_groups = await self._get("actionGroups") | ||
|
|
||
| if mask_sensitive_data: | ||
| setup = obfuscate_sensitive_data(setup) | ||
| action_groups = obfuscate_sensitive_data(action_groups) | ||
|
|
||
| return { | ||
| "setup": setup, | ||
| "action_groups": action_groups, | ||
| } | ||
| ``` | ||
|
|
||
| Also add `Any` to the `typing` import at the top of `client.py` if not already present. | ||
|
|
||
| - [ ] **Step 4: Run test to verify it passes** | ||
|
|
||
| Run: `pytest tests/test_client.py::TestOverkizClient::test_get_diagnostic_data_returns_structured_dict -v` | ||
| Expected: PASS. | ||
|
|
||
| - [ ] **Step 5: Commit** | ||
|
|
||
| ```bash | ||
| git add pyoverkiz/client.py tests/test_client.py | ||
| git commit -m "Return structured dict from get_diagnostic_data with action groups" | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### Task 3: Fix existing diagnostic tests for new return shape | ||
|
|
||
| **Files:** | ||
| - Modify: `tests/test_client.py:301-346` | ||
|
|
||
| The three existing diagnostic tests need updating to account for the new return shape (two API calls instead of one, structured dict output). | ||
|
|
||
| - [ ] **Step 1: Update `test_get_diagnostic_data`** | ||
|
|
||
| This parametrized test currently mocks a single `GET` call. It now needs to mock two (setup + actionGroups). Update to: | ||
|
|
||
| ```python | ||
| @pytest.mark.asyncio | ||
| async def test_get_diagnostic_data(self, client: OverkizClient, fixture_name: str): | ||
| """Verify that diagnostic data can be fetched and is not empty.""" | ||
| with (CURRENT_DIR / "fixtures" / "setup" / fixture_name).open( | ||
| encoding="utf-8", | ||
| ) as setup_mock: | ||
| setup_resp = MockResponse(setup_mock.read()) | ||
|
|
||
| with (CURRENT_DIR / "fixtures" / "action_groups" / "action-group-tahoma-switch.json").open( | ||
| encoding="utf-8", | ||
| ) as ag_mock: | ||
| ag_resp = MockResponse(ag_mock.read()) | ||
|
|
||
| responses = iter([setup_resp, ag_resp]) | ||
|
|
||
| with patch.object(aiohttp.ClientSession, "get", side_effect=lambda *a, **kw: next(responses)): | ||
| diagnostics = await client.get_diagnostic_data() | ||
| assert diagnostics | ||
| assert "setup" in diagnostics | ||
| assert "action_groups" in diagnostics | ||
| ``` | ||
|
|
||
| - [ ] **Step 2: Update `test_get_diagnostic_data_redacted_by_default`** | ||
|
|
||
| This test patches `obfuscate_sensitive_data` and needs to account for it being called twice (once for setup, once for action_groups): | ||
|
|
||
| ```python | ||
| @pytest.mark.asyncio | ||
| async def test_get_diagnostic_data_redacted_by_default(self, client: OverkizClient): | ||
| """Ensure diagnostics are redacted when no argument is provided.""" | ||
| with (CURRENT_DIR / "fixtures" / "setup" / "setup_tahoma_1.json").open( | ||
| encoding="utf-8", | ||
| ) as setup_mock: | ||
| setup_resp = MockResponse(setup_mock.read()) | ||
|
|
||
| with (CURRENT_DIR / "fixtures" / "action_groups" / "action-group-tahoma-switch.json").open( | ||
| encoding="utf-8", | ||
| ) as ag_mock: | ||
| ag_resp = MockResponse(ag_mock.read()) | ||
|
|
||
| responses = iter([setup_resp, ag_resp]) | ||
|
|
||
| with ( | ||
| patch.object(aiohttp.ClientSession, "get", side_effect=lambda *a, **kw: next(responses)), | ||
| patch( | ||
| "pyoverkiz.client.obfuscate_sensitive_data", | ||
| return_value={"masked": True}, | ||
| ) as obfuscate, | ||
| ): | ||
| diagnostics = await client.get_diagnostic_data() | ||
| assert diagnostics == { | ||
| "setup": {"masked": True}, | ||
| "action_groups": {"masked": True}, | ||
| } | ||
| assert obfuscate.call_count == 2 | ||
| ``` | ||
|
|
||
| - [ ] **Step 3: Update `test_get_diagnostic_data_without_masking`** | ||
|
|
||
| ```python | ||
| @pytest.mark.asyncio | ||
| async def test_get_diagnostic_data_without_masking(self, client: OverkizClient): | ||
| """Ensure diagnostics can be returned without masking when requested.""" | ||
| with (CURRENT_DIR / "fixtures" / "setup" / "setup_tahoma_1.json").open( | ||
| encoding="utf-8", | ||
| ) as setup_mock: | ||
| raw_setup = setup_mock.read() | ||
| setup_resp = MockResponse(raw_setup) | ||
|
|
||
| with (CURRENT_DIR / "fixtures" / "action_groups" / "action-group-tahoma-switch.json").open( | ||
| encoding="utf-8", | ||
| ) as ag_mock: | ||
| raw_ag = ag_mock.read() | ||
| ag_resp = MockResponse(raw_ag) | ||
|
|
||
| responses = iter([setup_resp, ag_resp]) | ||
|
|
||
| with ( | ||
| patch.object(aiohttp.ClientSession, "get", side_effect=lambda *a, **kw: next(responses)), | ||
| patch("pyoverkiz.client.obfuscate_sensitive_data") as obfuscate, | ||
| ): | ||
| diagnostics = await client.get_diagnostic_data(mask_sensitive_data=False) | ||
| assert diagnostics == { | ||
| "setup": json.loads(raw_setup), | ||
| "action_groups": json.loads(raw_ag), | ||
| } | ||
| obfuscate.assert_not_called() | ||
| ``` | ||
|
|
||
| - [ ] **Step 4: Run all diagnostic tests** | ||
|
|
||
| Run: `pytest tests/test_client.py -k "diagnostic" -v` | ||
| Expected: All 20+ parametrized tests PASS. | ||
|
|
||
| - [ ] **Step 5: Run full test suite** | ||
|
|
||
| Run: `pytest tests/ -v` | ||
| Expected: All tests PASS. | ||
|
|
||
| - [ ] **Step 6: Commit** | ||
|
|
||
| ```bash | ||
| git add tests/test_client.py | ||
| git commit -m "Update diagnostic tests for structured return shape" | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### Task 4: Document the breaking change in migration guide | ||
|
|
||
| **Files:** | ||
| - Modify: `docs/migration-v2.md` | ||
|
|
||
| - [ ] **Step 1: Add diagnostics section to migration guide** | ||
|
|
||
| Add a new section after the "Executing commands" section (around line 100) in `docs/migration-v2.md`: | ||
|
|
||
| ```markdown | ||
| ## Diagnostics | ||
|
|
||
| `get_diagnostic_data()` now returns a structured dict with named sections instead of a flat setup dump. | ||
|
|
||
| | v1 | v2 | | ||
| |----|-----| | ||
| | `data["gateways"]` | `data["setup"]["gateways"]` | | ||
| | (not available) | `data["action_groups"]` | | ||
|
|
||
| === "v1" | ||
|
|
||
| ```python | ||
| diagnostics = await client.get_diagnostic_data() | ||
| gateways = diagnostics["gateways"] | ||
| ``` | ||
|
|
||
| === "v2" | ||
|
|
||
| ```python | ||
| diagnostics = await client.get_diagnostic_data() | ||
| gateways = diagnostics["setup"]["gateways"] | ||
| action_groups = diagnostics["action_groups"] | ||
| ``` | ||
| ``` | ||
|
|
||
| - [ ] **Step 2: Commit** | ||
|
|
||
| ```bash | ||
| git add docs/migration-v2.md | ||
| git commit -m "Document get_diagnostic_data breaking change in migration guide" | ||
| ``` | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.