Skip to content

Align config-flow validation with GooglePollenApiClient, add quota exception, bump to v1.9.5#76

Closed
eXPerience83 wants to merge 1 commit intomainfrom
codex/refactor-input-validation-in-config-flow
Closed

Align config-flow validation with GooglePollenApiClient, add quota exception, bump to v1.9.5#76
eXPerience83 wants to merge 1 commit intomainfrom
codex/refactor-input-validation-in-config-flow

Conversation

@eXPerience83
Copy link
Owner

@eXPerience83 eXPerience83 commented Feb 27, 2026

User description

Motivation

  • Align config-flow setup validation with the runtime client behavior to ensure consistent parsing and error semantics.
  • Replace fragile text-based HTTP 429 detection with a dedicated client exception so quota_exceeded mapping is stable.
  • Improve maintainability by removing duplicate HTTP/JSON parsing from the flow and centralizing network logic in the client.
  • Prepare a release bump and document changes in CHANGELOG.md and package metadata.

Description

  • Added PollenQuotaExceededError in custom_components/pollenlevels/client.py and raise it when the API returns HTTP 429 instead of a text-based UpdateFailed.
  • Rewrote validation in custom_components/pollenlevels/config_flow.py to call GooglePollenApiClient.async_fetch_pollen_data and map ConfigEntryAuthFailed, PollenQuotaExceededError, UpdateFailed, TimeoutError, and ClientError to form error keys (invalid_auth, quota_exceeded, cannot_connect, etc.).
  • Updated unit test scaffolding in tests/test_config_flow.py to stub GooglePollenApiClient, add tests for client-timeout and client-transport error mapping, adapt helper factories, and validate that request args (like days and language_code) are normalized.
  • Bumped the integration version to 1.9.5 in manifest.json and pyproject.toml, and added the corresponding CHANGELOG.md entry.

Testing

  • Ran pytest tests/test_config_flow.py to exercise config-flow validation and client stubs, and the test suite passed.
  • Exercised new/updated assertions for HTTP 429 mapping (quota_exceeded), client TimeoutError mapping to cannot_connect, client transport errors mapping to cannot_connect, and the happy-path normalization, all of which succeeded.

Codex Task


PR Type

Enhancement, Tests


Description

  • Added PollenQuotaExceededError exception for dedicated HTTP 429 handling

  • Refactored config-flow validation to use GooglePollenApiClient directly

  • Centralized error mapping for auth, quota, timeout, and connectivity failures

  • Expanded test coverage with client-based validation and error scenarios

  • Bumped version to 1.9.5 with changelog documentation


Diagram Walkthrough

flowchart LR
  A["Config Flow Validation"] -->|"uses"| B["GooglePollenApiClient"]
  B -->|"raises"| C["PollenQuotaExceededError"]
  B -->|"raises"| D["ConfigEntryAuthFailed"]
  B -->|"raises"| E["UpdateFailed"]
  C -->|"maps to"| F["quota_exceeded"]
  D -->|"maps to"| G["invalid_auth"]
  E -->|"maps to"| H["cannot_connect"]
Loading

File Walkthrough

Relevant files
Enhancement
client.py
Add dedicated quota exceeded exception                                     

custom_components/pollenlevels/client.py

  • Added PollenQuotaExceededError exception class inheriting from
    UpdateFailed
  • Modified HTTP 429 handling to raise PollenQuotaExceededError instead
    of generic UpdateFailed
+5/-1     
config_flow.py
Refactor validation to use GooglePollenApiClient                 

custom_components/pollenlevels/config_flow.py

  • Removed manual HTTP request handling and JSON parsing from validation
  • Replaced with direct calls to
    GooglePollenApiClient.async_fetch_pollen_data
  • Added exception handlers for ConfigEntryAuthFailed,
    PollenQuotaExceededError, UpdateFailed, TimeoutError, and ClientError
  • Simplified error mapping logic by delegating to client exceptions
  • Removed unused imports (json, aiohttp, extract_error_message,
    is_invalid_api_key_message)
+38/-69 
Tests
test_config_flow.py
Update tests for client-based validation                                 

tests/test_config_flow.py

  • Added stub modules for homeassistant.exceptions and
    homeassistant.helpers.update_coordinator
  • Created _StubGooglePollenApiClient and _StubPollenQuotaExceededError
    for testing
  • Replaced _SequenceSession with _StubValidationClient and
    _PatchedClientFactory for client-based testing
  • Added test cases for client timeout mapping and client error mapping
    to cannot_connect
  • Updated existing tests to verify normalized request arguments (days,
    language_code)
+171/-25
Documentation
CHANGELOG.md
Document version 1.9.5 release changes                                     

CHANGELOG.md

  • Added version 1.9.5 release notes documenting alignment of config-flow
    with client semantics
  • Documented removal of duplicate HTTP/JSON parsing and centralized
    error mapping
  • Listed expanded test coverage for client-based setup validation
+12/-0   
Configuration changes
manifest.json
Bump integration version to 1.9.5                                               

custom_components/pollenlevels/manifest.json

  • Bumped version from 1.9.4 to 1.9.5
+1/-1     
pyproject.toml
Bump package version to 1.9.5                                                       

pyproject.toml

  • Bumped version from 1.9.4 to 1.9.5
+1/-1     

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the robustness and maintainability of the Google Pollen integration by standardizing API interaction during configuration. It ensures that setup validation mirrors runtime behavior, provides clearer error feedback for users, and centralizes API communication logic within the client. The changes also expand test coverage to prevent regressions in error handling and input normalization.

Highlights

  • Config-Flow Validation Alignment: The configuration flow validation has been refactored to use the GooglePollenApiClient directly, ensuring consistent parsing and error handling with the runtime client behavior. This centralizes network logic and removes duplicate HTTP/JSON parsing from the flow.
  • Dedicated Quota Exceeded Exception: A new PollenQuotaExceededError exception has been introduced in client.py to specifically handle HTTP 429 (Too Many Requests) responses from the Google Pollen API. This replaces a fragile text-based detection method, making quota error mapping more stable.
  • Enhanced Error Mapping: The config flow now explicitly maps various client exceptions, including ConfigEntryAuthFailed, PollenQuotaExceededError, UpdateFailed, TimeoutError, and ClientError, to appropriate form error keys like invalid_auth, quota_exceeded, and cannot_connect.
  • Improved Test Coverage: Unit tests for the config flow have been significantly updated to stub the GooglePollenApiClient, add specific tests for client-timeout and client-transport error mapping, adapt helper factories, and validate the normalization of request arguments like days and language_code.
  • Version Bump: The integration version has been updated to 1.9.5 across manifest.json, pyproject.toml, and CHANGELOG.md to reflect these changes.
Changelog
  • CHANGELOG.md
    • Added new entry for version 1.9.5.
    • Documented alignment of config-flow validation with runtime client semantics.
    • Noted expanded config-flow regression coverage.
    • Recorded replacement of text-based HTTP 429 detection with a dedicated client quota exception.
  • custom_components/pollenlevels/client.py
    • Added PollenQuotaExceededError class.
    • Modified async_fetch_pollen_data to raise PollenQuotaExceededError for HTTP 429 responses instead of UpdateFailed.
  • custom_components/pollenlevels/config_flow.py
    • Removed unused imports: json, aiohttp, is_invalid_api_key_message, extract_error_message.
    • Added imports for ClientError, ConfigEntryAuthFailed, UpdateFailed, GooglePollenApiClient, and PollenQuotaExceededError.
    • Refactored _async_validate_input to use GooglePollenApiClient.async_fetch_pollen_data for API calls.
    • Removed manual HTTP status code checking and JSON parsing, delegating to the client.
    • Implemented specific exception handling for ConfigEntryAuthFailed, PollenQuotaExceededError, UpdateFailed, TimeoutError, and ClientError to map to appropriate form error keys.
  • custom_components/pollenlevels/manifest.json
    • Updated the integration version to 1.9.5.
  • pyproject.toml
    • Updated the project version to 1.9.5.
  • tests/test_config_flow.py
    • Added stub classes for ConfigEntryAuthFailed and UpdateFailed.
    • Introduced _StubGooglePollenApiClient and _StubPollenQuotaExceededError for client mocking.
    • Refactored _patch_client_session to use _PatchedClientFactory for GooglePollenApiClient.
    • Added new test cases: test_validate_input_client_timeout_maps_to_cannot_connect and test_validate_input_client_error_maps_to_cannot_connect.
    • Updated existing happy path tests to assert on GooglePollenApiClient calls and argument normalization.
Activity
  • The author ran pytest tests/test_config_flow.py to verify config-flow validation and client stubs.
  • New and updated assertions for HTTP 429 mapping (quota_exceeded), client TimeoutError mapping to cannot_connect, client transport errors mapping to cannot_connect, and happy-path normalization were exercised and passed.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@qodo-code-review
Copy link
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Error detail exposure

Description: The config flow copies exception text (potentially containing upstream/server-provided
error details) into user-facing form placeholders via placeholders["error_message"], which
could expose sensitive or overly-detailed internal information even though the API key is
redacted.
config_flow.py [435-449]

Referred Code
except ConfigEntryAuthFailed as err:
    errors["base"] = "invalid_auth"
    redacted = redact_api_key(err, api_key)
    if redacted:
        placeholders["error_message"] = redacted
except PollenQuotaExceededError as err:
    redacted = redact_api_key(err, api_key)
    if redacted:
        placeholders["error_message"] = redacted
    errors["base"] = "quota_exceeded"
except UpdateFailed as err:
    redacted = redact_api_key(err, api_key)
    if redacted:
        placeholders["error_message"] = redacted
    errors["base"] = "cannot_connect"
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
User-facing details: The flow populates placeholders["error_message"] with exception text (e.g., HTTP
status/body-derived messages) which may be user-visible and could leak internal/backend
details beyond a generic error.

Referred Code
except ConfigEntryAuthFailed as err:
    errors["base"] = "invalid_auth"
    redacted = redact_api_key(err, api_key)
    if redacted:
        placeholders["error_message"] = redacted
except PollenQuotaExceededError as err:
    redacted = redact_api_key(err, api_key)
    if redacted:
        placeholders["error_message"] = redacted
    errors["base"] = "quota_exceeded"
except UpdateFailed as err:
    redacted = redact_api_key(err, api_key)
    if redacted:
        placeholders["error_message"] = redacted
    errors["base"] = "cannot_connect"

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured logging: New/updated log statements are plain-text (not structured/JSON) and include redacted
exception strings, so confirmation is needed that logging output and sinks meet the
project's structured-logging expectations.

Referred Code
        _LOGGER.warning("Validation: 'dailyInfo' missing or invalid")
        errors["base"] = "cannot_connect"
        placeholders["error_message"] = (
            "API response missing expected pollen forecast information."
        )

    if errors:
        return errors, None

    normalized[CONF_LANGUAGE_CODE] = lang
    return errors, normalized

except vol.Invalid as ve:
    _LOGGER.warning(
        "Language code validation failed for '%s': %s",
        user_input.get(CONF_LANGUAGE_CODE),
        ve,
    )
    errors[CONF_LANGUAGE_CODE] = _language_error_to_form_key(ve)
    placeholders.pop("error_message", None)
except ConfigEntryAuthFailed as err:


 ... (clipped 31 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review
Copy link
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
General
Consolidate redundant exception handling logic

Consolidate the exception handling for ConfigEntryAuthFailed,
PollenQuotaExceededError, and UpdateFailed to remove redundant code for
redacting API keys and setting error messages.

custom_components/pollenlevels/config_flow.py [435-449]

-+        except ConfigEntryAuthFailed as err:
-+            errors["base"] = "invalid_auth"
++        except (ConfigEntryAuthFailed, PollenQuotaExceededError, UpdateFailed) as err:
++            if isinstance(err, ConfigEntryAuthFailed):
++                errors["base"] = "invalid_auth"
++            elif isinstance(err, PollenQuotaExceededError):
++                errors["base"] = "quota_exceeded"
++            else:
++                errors["base"] = "cannot_connect"
++
 +            redacted = redact_api_key(err, api_key)
 +            if redacted:
 +                placeholders["error_message"] = redacted
-+        except PollenQuotaExceededError as err:
-+            redacted = redact_api_key(err, api_key)
-+            if redacted:
-+                placeholders["error_message"] = redacted
-+            errors["base"] = "quota_exceeded"
-+        except UpdateFailed as err:
-+            redacted = redact_api_key(err, api_key)
-+            if redacted:
-+                placeholders["error_message"] = redacted
-+            errors["base"] = "cannot_connect"

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies duplicated code and proposes a valid refactoring that improves code quality and maintainability without changing functionality.

Low
Avoid calling the same function twice

In the TimeoutError exception handler, call redact_api_key only once, store its
result in a variable, and reuse it for both logging and setting the error
message to avoid a redundant call.

custom_components/pollenlevels/config_flow.py [450-461]

 except TimeoutError as err:
+    redacted = redact_api_key(err, api_key)
     _LOGGER.warning(
         "Validation timeout (%ss): %s",
         POLLEN_API_TIMEOUT,
-        redact_api_key(err, api_key),
+        redacted,
     )
     errors["base"] = "cannot_connect"
-    redacted = redact_api_key(err, api_key)
     placeholders["error_message"] = (
         redacted
         or f"Validation request timed out ({POLLEN_API_TIMEOUT} seconds)."
     )
  • Apply / Chat
Suggestion importance[1-10]: 4

__

Why: The suggestion correctly points out a redundant function call and proposes a valid optimization that improves efficiency and code clarity, which is a good practice.

Low
  • More

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively refactors the config flow to use the GooglePollenApiClient, which centralizes network and error handling logic, improving maintainability. The introduction of a dedicated PollenQuotaExceededError is a good enhancement for clearer error semantics. My review includes a high-severity comment on the test implementation, suggesting a refactor to better align with the new decoupled architecture by mocking the client's interface rather than its implementation, consistent with best practices for mocking. I've also included a medium-severity suggestion to improve code conciseness in the new exception handling blocks.

Comment on lines +383 to +441
class _StubValidationClient:
def __init__(self, responses: list[_StubResponse], api_key: str) -> None:
self._responses = responses
self._api_key = api_key
self.calls: list[dict[str, object]] = []

async def read(self) -> bytes:
return self._body
async def async_fetch_pollen_data(
self,
*,
latitude: float,
longitude: float,
days: int,
language_code: str | None,
) -> dict:
self.calls.append(
{
"latitude": latitude,
"longitude": longitude,
"days": days,
"language_code": language_code,
}
)
response = self._responses.pop(0)

if response.status == 401:
raise cf.ConfigEntryAuthFailed("HTTP 401: API key *** not valid")

if response.status == 403:
body_text = response._body.decode()
if "API key not valid" in body_text:
raise cf.ConfigEntryAuthFailed(f"HTTP 403: {body_text}")
raise cf.UpdateFailed(f"HTTP 403: {body_text}")

if response.status == 429:
raise cf.PollenQuotaExceededError("HTTP 429: Quota exceeded")

if response.status != 200:
raise cf.UpdateFailed(f"HTTP {response.status}: backend error")

async def json(self):
import json as _json

return _json.loads(self._body.decode())
return _json.loads(response._body.decode())

async def text(self) -> str:
return self._body.decode()

class _PatchedClientFactory:
def __init__(self, responses: list[_StubResponse]):
self._responses = responses
self.instances: list[_StubValidationClient] = []

class _SequenceSession:
def __init__(self, responses: list[_StubResponse]) -> None:
self.responses = responses
self.calls: list[tuple[tuple, dict]] = []
@property
def calls(self) -> list[dict[str, object]]:
if not self.instances:
return []
return self.instances[0].calls

def get(self, *args, **kwargs):
self.calls.append((args, kwargs))
return self.responses.pop(0)
def __call__(self, session, api_key: str) -> _StubValidationClient:
client = _StubValidationClient(self._responses, api_key)
self.instances.append(client)
return client
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The _StubValidationClient re-implements logic for interpreting HTTP responses and raising exceptions. This duplicates the responsibility of GooglePollenApiClient, which this PR aims to centralize. The tests for config_flow should be decoupled from the client's internal implementation (i.e., how it handles HTTP status codes). Instead, the mock client should directly raise the expected exceptions (ConfigEntryAuthFailed, PollenQuotaExceededError, etc.) to test how config_flow handles them. The _TimeoutClient and _ClientErroringClient classes in this file are great examples of this preferred approach. Refactoring the tests to follow this pattern will make them more robust and better aligned with the goal of this PR.

References
  1. Mocks should accurately reflect the synchronicity and overall behavior of the function being mocked. By directly raising expected exceptions, the mock accurately simulates the outcome of the real client's methods, aligning with the principle of mocking the interface rather than internal implementation details.

Comment on lines +435 to +449
except ConfigEntryAuthFailed as err:
errors["base"] = "invalid_auth"
redacted = redact_api_key(err, api_key)
if redacted:
placeholders["error_message"] = redacted
except PollenQuotaExceededError as err:
redacted = redact_api_key(err, api_key)
if redacted:
placeholders["error_message"] = redacted
errors["base"] = "quota_exceeded"
except UpdateFailed as err:
redacted = redact_api_key(err, api_key)
if redacted:
placeholders["error_message"] = redacted
errors["base"] = "cannot_connect"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To reduce code duplication and improve conciseness, you can use assignment expressions (the walrus operator :=) to both check for and assign the redacted error message. This is supported since your project requires Python >= 3.14.

Suggested change
except ConfigEntryAuthFailed as err:
errors["base"] = "invalid_auth"
redacted = redact_api_key(err, api_key)
if redacted:
placeholders["error_message"] = redacted
except PollenQuotaExceededError as err:
redacted = redact_api_key(err, api_key)
if redacted:
placeholders["error_message"] = redacted
errors["base"] = "quota_exceeded"
except UpdateFailed as err:
redacted = redact_api_key(err, api_key)
if redacted:
placeholders["error_message"] = redacted
errors["base"] = "cannot_connect"
except ConfigEntryAuthFailed as err:
errors["base"] = "invalid_auth"
if redacted := redact_api_key(err, api_key):
placeholders["error_message"] = redacted
except PollenQuotaExceededError as err:
errors["base"] = "quota_exceeded"
if redacted := redact_api_key(err, api_key):
placeholders["error_message"] = redacted
except UpdateFailed as err:
errors["base"] = "cannot_connect"
if redacted := redact_api_key(err, api_key):
placeholders["error_message"] = redacted

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8aaf555232

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".



update_coordinator_mod.UpdateFailed = _StubUpdateFailed
_force_module("homeassistant.helpers.update_coordinator", update_coordinator_mod)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop clobbering shared update_coordinator test module

Forcing homeassistant.helpers.update_coordinator into sys.modules with only UpdateFailed makes the test process order-dependent: if this file is imported before other test modules, later imports of DataUpdateCoordinator fail during collection. In this repo, pytest -q now errors with ImportError: cannot import name 'DataUpdateCoordinator' from tests/test_init.py/tests/test_sensor.py, so this change breaks the full suite rather than just isolating config-flow tests.

Useful? React with 👍 / 👎.


client_mod.GooglePollenApiClient = _StubGooglePollenApiClient
client_mod.PollenQuotaExceededError = _StubPollenQuotaExceededError
_force_module("custom_components.pollenlevels.client", client_mod)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop replacing the real client module in test bootstrap

Injecting a fake custom_components.pollenlevels.client module globally causes downstream tests to run against the stub instead of the real client whenever this module is imported first. That leak breaks sensor/coordinator tests that rely on real client attributes/behavior (for example, tests patching client_mod.asyncio fail with AttributeError), so the suite becomes import-order dependent and no longer validates production client logic.

Useful? React with 👍 / 👎.

@eXPerience83 eXPerience83 deleted the codex/refactor-input-validation-in-config-flow branch February 27, 2026 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant