From 99a984535b39c998fadd470b45865f0e1a6429c1 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Sat, 15 Nov 2025 13:37:27 -0800 Subject: [PATCH 1/2] Fix DNS resolution in containerized environments using ThreadedResolver - Replace default aiodns resolver with explicit ThreadedResolver - Uses Python's built-in socket module instead of c-ares binding - Fixes DNS failures in Home Assistant and other containers - No additional dependencies required - Minimal changes to 3 session creation points: * NavienAuthClient.__aenter__() * NavienAuthClient._ensure_session() * refresh_access_token() function Resolves DNS errors: 'DNS server returned general failure' Makes library work reliably in all containerized environments --- src/nwp500/auth.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/nwp500/auth.py b/src/nwp500/auth.py index a71f39b..dd08a15 100644 --- a/src/nwp500/auth.py +++ b/src/nwp500/auth.py @@ -367,7 +367,11 @@ def __init__( async def __aenter__(self) -> "NavienAuthClient": """Async context manager entry.""" if self._owned_session: - self._session = aiohttp.ClientSession(timeout=self.timeout) + resolver = aiohttp.ThreadedResolver() + connector = aiohttp.TCPConnector(resolver=resolver) + self._session = aiohttp.ClientSession( + connector=connector, timeout=self.timeout + ) # Check if we have valid stored tokens if self._auth_response and self._auth_response.tokens: @@ -397,7 +401,11 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: async def _ensure_session(self) -> None: """Ensure we have an active session.""" if self._session is None: - self._session = aiohttp.ClientSession(timeout=self.timeout) + resolver = aiohttp.ThreadedResolver() + connector = aiohttp.TCPConnector(resolver=resolver) + self._session = aiohttp.ClientSession( + connector=connector, timeout=self.timeout + ) self._owned_session = True async def sign_in( @@ -740,8 +748,10 @@ async def refresh_access_token(refresh_token: str) -> AuthTokens: url = f"{API_BASE_URL}{REFRESH_ENDPOINT}" payload = {"refreshToken": refresh_token} + resolver = aiohttp.ThreadedResolver() + connector = aiohttp.TCPConnector(resolver=resolver) async with ( - aiohttp.ClientSession() as session, + aiohttp.ClientSession(connector=connector) as session, session.post(url, json=payload) as response, ): response_data = await response.json() From ca1773c62b532d2675d04fb98ba8132858161fef Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Sat, 15 Nov 2025 13:45:25 -0800 Subject: [PATCH 2/2] Refactor: Extract ThreadedResolver session creation into helper method - Add _create_session() helper method to NavienAuthClient - Eliminates code duplication across 3 session creation points - Improves maintainability and makes future changes easier - All 3 locations now use single source of truth for DNS resolver config - Maintains same functionality with cleaner code --- src/nwp500/auth.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/nwp500/auth.py b/src/nwp500/auth.py index dd08a15..fadd188 100644 --- a/src/nwp500/auth.py +++ b/src/nwp500/auth.py @@ -367,11 +367,7 @@ def __init__( async def __aenter__(self) -> "NavienAuthClient": """Async context manager entry.""" if self._owned_session: - resolver = aiohttp.ThreadedResolver() - connector = aiohttp.TCPConnector(resolver=resolver) - self._session = aiohttp.ClientSession( - connector=connector, timeout=self.timeout - ) + self._session = self._create_session() # Check if we have valid stored tokens if self._auth_response and self._auth_response.tokens: @@ -398,14 +394,24 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: if self._owned_session and self._session: await self._session.close() + def _create_session(self) -> aiohttp.ClientSession: + """Create an aiohttp ClientSession with ThreadedResolver. + + ThreadedResolver uses Python's built-in socket module for DNS + resolution, avoiding dependency on c-ares which can fail in + containerized environments. + + Returns: + aiohttp.ClientSession configured with ThreadedResolver + """ + resolver = aiohttp.ThreadedResolver() + connector = aiohttp.TCPConnector(resolver=resolver) + return aiohttp.ClientSession(connector=connector, timeout=self.timeout) + async def _ensure_session(self) -> None: """Ensure we have an active session.""" if self._session is None: - resolver = aiohttp.ThreadedResolver() - connector = aiohttp.TCPConnector(resolver=resolver) - self._session = aiohttp.ClientSession( - connector=connector, timeout=self.timeout - ) + self._session = self._create_session() self._owned_session = True async def sign_in( @@ -748,6 +754,7 @@ async def refresh_access_token(refresh_token: str) -> AuthTokens: url = f"{API_BASE_URL}{REFRESH_ENDPOINT}" payload = {"refreshToken": refresh_token} + # Use ThreadedResolver for reliable DNS in containerized environments resolver = aiohttp.ThreadedResolver() connector = aiohttp.TCPConnector(resolver=resolver) async with (