Skip to content

Commit 69a2179

Browse files
pmcfadinclaude
andcommitted
fix: suppress astrapy UNSUPPORTED_TABLE_COMMAND warning in safe_count
countDocuments is unsupported on CQL tables, and astrapy logs a WARNING before raising DataAPIResponseException. safe_count already catches and handles this case correctly (returning fallback_len), but the warning appeared in logs as a false alarm. Fix: attach a targeted logging.Filter to the astrapy.utils.api_commander logger during the count_documents call. The filter drops only records containing UNSUPPORTED_TABLE_COMMAND; all other errors still surface. Also fix conftest.py: was installing astrapy stubs even when astrapy is installed, because the sys.modules check fired before any app code imported astrapy. Now uses importlib.util.find_spec() to check installability, not presence in sys.modules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a107d2f commit 69a2179

3 files changed

Lines changed: 120 additions & 2 deletions

File tree

app/utils/db_helpers.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,27 @@
1313
object.
1414
"""
1515

16+
import logging
1617
from typing import Any, Dict
1718

1819
from astrapy.exceptions.data_api_exceptions import DataAPIResponseException # type: ignore
1920

2021
__all__ = ["safe_count"]
2122

23+
_ASTRAPY_LOGGER = logging.getLogger("astrapy.utils.api_commander")
24+
25+
26+
class _SuppressUnsupportedTableCommand(logging.Filter):
27+
"""Drop UNSUPPORTED_TABLE_COMMAND WARNING records from the astrapy logger.
28+
29+
astrapy emits a WARNING before raising DataAPIResponseException. For the
30+
expected case of ``countDocuments`` on a CQL table we catch and handle the
31+
exception ourselves, so the warning is noise rather than signal.
32+
"""
33+
34+
def filter(self, record: logging.LogRecord) -> bool:
35+
return "UNSUPPORTED_TABLE_COMMAND" not in record.getMessage()
36+
2237

2338
async def safe_count(
2439
db_table,
@@ -33,12 +48,16 @@ async def safe_count(
3348
an exception. The same applies to stub collections used in unit-tests.
3449
"""
3550

51+
_filter = _SuppressUnsupportedTableCommand()
52+
_ASTRAPY_LOGGER.addFilter(_filter)
3653
try:
3754
return await db_table.count_documents(filter=query_filter, upper_bound=10**9)
38-
except (TypeError, DataAPIResponseException) as exc: # pragma: no cover – fallback
55+
except (TypeError, DataAPIResponseException) as exc:
3956
if isinstance(
4057
exc, DataAPIResponseException
4158
) and "UNSUPPORTED_TABLE_COMMAND" not in str(exc):
4259
# An unexpected Data API error – surface to caller.
4360
raise
4461
return fallback_len
62+
finally:
63+
_ASTRAPY_LOGGER.removeFilter(_filter)

tests/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
# ---------------------------------------------------------------------------
77
# Stub for `astrapy` when the real package is not installed (CI / unit tests)
88
# ---------------------------------------------------------------------------
9-
if "astrapy" not in sys.modules: # pragma: no cover
9+
import importlib.util as _importlib_util
10+
if _importlib_util.find_spec("astrapy") is None: # pragma: no cover
1011
astrapy_stub = types.ModuleType("astrapy")
1112
db_stub = types.ModuleType("astrapy.db")
1213

tests/utils/test_db_helpers.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Tests for app.utils.db_helpers.safe_count."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
7+
import pytest
8+
from unittest.mock import AsyncMock
9+
10+
from astrapy.exceptions.data_api_exceptions import DataAPIResponseException # type: ignore[import]
11+
12+
from app.utils.db_helpers import safe_count
13+
14+
_ASTRAPY_LOGGER = "astrapy.utils.api_commander"
15+
16+
17+
def _make_exc(error_code: str) -> DataAPIResponseException:
18+
"""Build a DataAPIResponseException whose str() contains the given error code."""
19+
return DataAPIResponseException(
20+
error_code,
21+
command={},
22+
raw_response={"errors": [{"errorCode": error_code, "message": error_code}]},
23+
error_descriptors=[],
24+
warning_descriptors=[],
25+
)
26+
27+
28+
# ---------------------------------------------------------------------------
29+
# Correct fallback behaviour
30+
# ---------------------------------------------------------------------------
31+
32+
33+
@pytest.mark.asyncio
34+
async def test_safe_count_returns_fallback_for_unsupported_table_command():
35+
"""Returns fallback_len when the table doesn't support countDocuments."""
36+
db_table = AsyncMock()
37+
db_table.count_documents.side_effect = _make_exc("UNSUPPORTED_TABLE_COMMAND")
38+
39+
result = await safe_count(db_table, query_filter={}, fallback_len=7)
40+
41+
assert result == 7
42+
43+
44+
@pytest.mark.asyncio
45+
async def test_safe_count_returns_actual_count_when_supported():
46+
"""Returns the real count when count_documents succeeds."""
47+
db_table = AsyncMock()
48+
db_table.count_documents.return_value = 42
49+
50+
result = await safe_count(db_table, query_filter={"userid": "abc"}, fallback_len=3)
51+
52+
assert result == 42
53+
54+
55+
@pytest.mark.asyncio
56+
async def test_safe_count_propagates_unexpected_data_api_error():
57+
"""Re-raises DataAPIResponseException for unrelated error codes."""
58+
db_table = AsyncMock()
59+
db_table.count_documents.side_effect = _make_exc("SOME_OTHER_ERROR")
60+
61+
with pytest.raises(DataAPIResponseException):
62+
await safe_count(db_table, query_filter={}, fallback_len=5)
63+
64+
65+
# ---------------------------------------------------------------------------
66+
# Warning suppression — simulates astrapy's real behaviour (log then raise)
67+
# ---------------------------------------------------------------------------
68+
69+
70+
@pytest.mark.asyncio
71+
async def test_safe_count_does_not_log_warning_for_unsupported_table_command(caplog):
72+
"""UNSUPPORTED_TABLE_COMMAND must NOT produce a WARNING in the logs.
73+
74+
astrapy logs a WARNING from api_commander *before* raising the exception.
75+
We simulate that by having the mock emit the warning then raise, matching
76+
what happens in production when count_documents hits a CQL table.
77+
"""
78+
astrapy_logger = logging.getLogger(_ASTRAPY_LOGGER)
79+
exc = _make_exc("UNSUPPORTED_TABLE_COMMAND")
80+
81+
async def _fake_count_documents(*args, **kwargs):
82+
astrapy_logger.warning("APICommander about to raise from: UNSUPPORTED_TABLE_COMMAND")
83+
raise exc
84+
85+
db_table = AsyncMock()
86+
db_table.count_documents = _fake_count_documents
87+
88+
with caplog.at_level(logging.WARNING, logger=_ASTRAPY_LOGGER):
89+
await safe_count(db_table, query_filter={}, fallback_len=3)
90+
91+
unsupported_warnings = [
92+
r for r in caplog.records
93+
if "UNSUPPORTED_TABLE_COMMAND" in r.getMessage()
94+
and r.levelno >= logging.WARNING
95+
]
96+
assert unsupported_warnings == [], (
97+
"safe_count should suppress astrapy's UNSUPPORTED_TABLE_COMMAND warning"
98+
)

0 commit comments

Comments
 (0)