Skip to content

Commit 0b6d2df

Browse files
test(10-01): add unit tests for SessionBase ABC contract and complexity
- ConcreteSession test helper implementing abstract methods - ABC enforcement, config storage, _build_headers tests - Status dispatch tests: 200, 301, 429, 500, 4xx variants - Simulate mode, retry exhaustion tests - Complexity audit (all handlers verified < 10) - Refactored _handle_client_error to extract _classify_client_error_wait and _retry_with_wait
1 parent 8a568bf commit 0b6d2df

2 files changed

Lines changed: 388 additions & 42 deletions

File tree

meraki/session/base.py

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -327,57 +327,23 @@ def _handle_client_error(
327327
328328
Raises APIError if error is not retryable or retries exhausted.
329329
"""
330-
tag = metadata["tags"][0]
331-
operation = metadata["operation"]
332-
reason = response.reason_phrase if hasattr(response, "reason_phrase") else ""
333-
status = response.status_code
334-
335330
# Parse response body
336331
try:
337332
message = response.json()
338333
except (ValueError, json.decoder.JSONDecodeError):
339334
message = response.content[:100]
340335

341-
# Network delete concurrency error
342-
if self._is_network_delete_concurrency(metadata, response, message):
343-
wait = random.randint(30, self._network_delete_retry_wait_time)
344-
if self._logger:
345-
self._logger.warning(
346-
f"{tag}, {operation} - {status} {reason}, retrying in {wait} seconds"
347-
)
348-
self._sleep(wait)
349-
retries -= 1
350-
if retries == 0:
351-
raise APIError(metadata, response)
352-
return retries
336+
# Determine wait time based on error type
337+
wait = self._classify_client_error_wait(metadata, response, message)
353338

354-
# Action batch concurrency error
355-
if self._is_action_batch_concurrency(message):
356-
wait = self._action_batch_retry_wait_time
357-
if self._logger:
358-
self._logger.warning(
359-
f"{tag}, {operation} - {status} {reason}, retrying in {wait} seconds"
360-
)
361-
self._sleep(wait)
362-
retries -= 1
363-
if retries == 0:
364-
raise APIError(metadata, response)
365-
return retries
366-
367-
# Generic 4xx retry
368-
if self._retry_4xx_error and retries > 0:
369-
wait = random.randint(1, self._retry_4xx_error_wait_time)
370-
if self._logger:
371-
self._logger.warning(
372-
f"{tag}, {operation} - {status} {reason}, retrying in {wait} seconds"
373-
)
374-
self._sleep(wait)
375-
retries -= 1
376-
if retries == 0:
377-
raise APIError(metadata, response)
378-
return retries
339+
if wait is not None:
340+
return self._retry_with_wait(wait, metadata, response, retries)
379341

380342
# Non-retryable client error
343+
tag = metadata["tags"][0]
344+
operation = metadata["operation"]
345+
reason = response.reason_phrase if hasattr(response, "reason_phrase") else ""
346+
status = response.status_code
381347
if self._logger:
382348
self._logger.error(f"{tag}, {operation} - {status} {reason}, {message}")
383349
raise APIError(metadata, response)
@@ -386,6 +352,44 @@ def _handle_client_error(
386352
# Helper methods
387353
# ------------------------------------------------------------------
388354

355+
def _classify_client_error_wait(
356+
self,
357+
metadata: Dict[str, Any],
358+
response: "httpx.Response",
359+
message: Any,
360+
) -> Optional[float]:
361+
"""Determine retry wait time for a 4xx error, or None if non-retryable."""
362+
if self._is_network_delete_concurrency(metadata, response, message):
363+
return float(random.randint(30, self._network_delete_retry_wait_time))
364+
if self._is_action_batch_concurrency(message):
365+
return float(self._action_batch_retry_wait_time)
366+
if self._retry_4xx_error:
367+
return float(random.randint(1, self._retry_4xx_error_wait_time))
368+
return None
369+
370+
def _retry_with_wait(
371+
self,
372+
wait: float,
373+
metadata: Dict[str, Any],
374+
response: "httpx.Response",
375+
retries: int,
376+
) -> int:
377+
"""Log, sleep, decrement retries; raise APIError if exhausted."""
378+
tag = metadata["tags"][0]
379+
operation = metadata["operation"]
380+
reason = response.reason_phrase if hasattr(response, "reason_phrase") else ""
381+
status = response.status_code
382+
383+
if self._logger:
384+
self._logger.warning(
385+
f"{tag}, {operation} - {status} {reason}, retrying in {wait} seconds"
386+
)
387+
self._sleep(wait)
388+
retries -= 1
389+
if retries == 0:
390+
raise APIError(metadata, response)
391+
return retries
392+
389393
def _is_network_delete_concurrency(
390394
self,
391395
metadata: Dict[str, Any],

0 commit comments

Comments
 (0)