fix: resolve all type checking errors (8 errors fixed)
Fixed 8 type checker errors across the codebase:
- vector/scanner.py: Handle None scroll results with null-safe iteration
- search/{bm25_hybrid,semantic}.py: Add None checks for result.payload
- auth/{unified_verifier,webhook_routes}.py: Assert non-None auth credentials
- client/webdav.py: Add None checks before int() conversions
- providers/openai.py: Assert embedding_model is not None
- search/algorithms.py: Explicitly type doc_types set and cast values
- observability/logging_config.py: Match parent class signature (log_data)
Also fixed test_create_tag_creates_system_tag to match WebDAV implementation
(was testing OCS API endpoint, now tests correct WebDAV endpoint with
Content-Location header).
Type checker: 0 errors (down from 8), 20 warnings (ignored)
Tests: All 192 unit tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -303,10 +303,13 @@ class UnifiedTokenVerifier(TokenVerifier):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Introspection requires client authentication
|
# Introspection requires client authentication
|
||||||
|
client_id = self.settings.oidc_client_id
|
||||||
|
client_secret = self.settings.oidc_client_secret
|
||||||
|
assert client_id is not None and client_secret is not None
|
||||||
response = await self.http_client.post(
|
response = await self.http_client.post(
|
||||||
self.introspection_uri,
|
self.introspection_uri,
|
||||||
data={"token": token},
|
data={"token": token},
|
||||||
auth=(self.settings.oidc_client_id, self.settings.oidc_client_secret),
|
auth=(client_id, client_secret),
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ async def _get_authenticated_client(request: Request) -> httpx.AsyncClient:
|
|||||||
raise RuntimeError("BasicAuth credentials not configured")
|
raise RuntimeError("BasicAuth credentials not configured")
|
||||||
|
|
||||||
assert nextcloud_host is not None # Type narrowing for type checker
|
assert nextcloud_host is not None # Type narrowing for type checker
|
||||||
|
assert username is not None and password is not None # Type narrowing
|
||||||
return httpx.AsyncClient(
|
return httpx.AsyncClient(
|
||||||
base_url=nextcloud_host,
|
base_url=nextcloud_host,
|
||||||
auth=(username, password),
|
auth=(username, password),
|
||||||
|
|||||||
@@ -1174,7 +1174,9 @@ class WebDAVClient(BaseNextcloudClient):
|
|||||||
|
|
||||||
if display_name_elem is not None and display_name_elem.text == tag_name:
|
if display_name_elem is not None and display_name_elem.text == tag_name:
|
||||||
tag_info = {
|
tag_info = {
|
||||||
"id": int(tag_id_elem.text) if tag_id_elem is not None else None,
|
"id": int(tag_id_elem.text)
|
||||||
|
if tag_id_elem is not None and tag_id_elem.text is not None
|
||||||
|
else None,
|
||||||
"name": display_name_elem.text,
|
"name": display_name_elem.text,
|
||||||
"userVisible": user_visible_elem.text.lower() == "true"
|
"userVisible": user_visible_elem.text.lower() == "true"
|
||||||
if user_visible_elem is not None
|
if user_visible_elem is not None
|
||||||
@@ -1369,7 +1371,9 @@ class WebDAVClient(BaseNextcloudClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
file_info = {
|
file_info = {
|
||||||
"id": int(fileid_elem.text) if fileid_elem is not None else None,
|
"id": int(fileid_elem.text)
|
||||||
|
if fileid_elem is not None and fileid_elem.text is not None
|
||||||
|
else None,
|
||||||
"path": path,
|
"path": path,
|
||||||
"name": displayname_elem.text
|
"name": displayname_elem.text
|
||||||
if displayname_elem is not None
|
if displayname_elem is not None
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class TraceContextFormatter(JsonFormatter):
|
|||||||
|
|
||||||
def add_fields(
|
def add_fields(
|
||||||
self,
|
self,
|
||||||
log_record: dict[str, Any],
|
log_data: dict[str, Any],
|
||||||
record: logging.LogRecord,
|
record: logging.LogRecord,
|
||||||
message_dict: dict[str, Any],
|
message_dict: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -68,28 +68,28 @@ class TraceContextFormatter(JsonFormatter):
|
|||||||
Add custom fields to the log record, including trace context.
|
Add custom fields to the log record, including trace context.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
log_record: Dictionary to be serialized as JSON
|
log_data: Dictionary to be serialized as JSON
|
||||||
record: LogRecord instance
|
record: LogRecord instance
|
||||||
message_dict: Dictionary of extra fields from log call
|
message_dict: Dictionary of extra fields from log call
|
||||||
"""
|
"""
|
||||||
# Call parent to add standard fields
|
# Call parent to add standard fields
|
||||||
super().add_fields(log_record, record, message_dict)
|
super().add_fields(log_data, record, message_dict)
|
||||||
|
|
||||||
# Add trace context if available
|
# Add trace context if available
|
||||||
trace_context = get_trace_context()
|
trace_context = get_trace_context()
|
||||||
if trace_context:
|
if trace_context:
|
||||||
log_record["trace_id"] = trace_context.get("trace_id")
|
log_data["trace_id"] = trace_context.get("trace_id")
|
||||||
log_record["span_id"] = trace_context.get("span_id")
|
log_data["span_id"] = trace_context.get("span_id")
|
||||||
|
|
||||||
# Add standard fields with consistent naming
|
# Add standard fields with consistent naming
|
||||||
log_record["timestamp"] = self.formatTime(record)
|
log_data["timestamp"] = self.formatTime(record)
|
||||||
log_record["level"] = record.levelname
|
log_data["level"] = record.levelname
|
||||||
log_record["logger"] = record.name
|
log_data["logger"] = record.name
|
||||||
log_record["message"] = record.getMessage()
|
log_data["message"] = record.getMessage()
|
||||||
|
|
||||||
# Include exception info if present
|
# Include exception info if present
|
||||||
if record.exc_info:
|
if record.exc_info:
|
||||||
log_record["exception"] = self.formatException(record.exc_info)
|
log_data["exception"] = self.formatException(record.exc_info)
|
||||||
|
|
||||||
|
|
||||||
class TraceContextTextFormatter(logging.Formatter):
|
class TraceContextTextFormatter(logging.Formatter):
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ class OpenAIProvider(Provider):
|
|||||||
"Embedding not supported - no embedding_model configured"
|
"Embedding not supported - no embedding_model configured"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert self.embedding_model is not None # Type narrowing
|
||||||
response = await self.client.embeddings.create(
|
response = await self.client.embeddings.create(
|
||||||
input=text,
|
input=text,
|
||||||
model=self.embedding_model,
|
model=self.embedding_model,
|
||||||
@@ -204,6 +205,7 @@ class OpenAIProvider(Provider):
|
|||||||
@retry_on_rate_limit
|
@retry_on_rate_limit
|
||||||
async def _embed_batch_request(self, batch: list[str]) -> list[list[float]]:
|
async def _embed_batch_request(self, batch: list[str]) -> list[list[float]]:
|
||||||
"""Make a single batch embedding request with retry logic."""
|
"""Make a single batch embedding request with retry logic."""
|
||||||
|
assert self.embedding_model is not None # Type narrowing
|
||||||
response = await self.client.embeddings.create(
|
response = await self.client.embeddings.create(
|
||||||
input=batch,
|
input=batch,
|
||||||
model=self.embedding_model,
|
model=self.embedding_model,
|
||||||
|
|||||||
@@ -108,8 +108,8 @@ async def get_indexed_doc_types(user_id: str) -> set[str]:
|
|||||||
with_vectors=False, # Don't need vectors for type discovery
|
with_vectors=False, # Don't need vectors for type discovery
|
||||||
)
|
)
|
||||||
|
|
||||||
doc_types = {
|
doc_types: set[str] = {
|
||||||
point.payload.get("doc_type")
|
str(point.payload.get("doc_type"))
|
||||||
for point in scroll_results
|
for point in scroll_results
|
||||||
if point.payload.get("doc_type")
|
if point.payload.get("doc_type")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,6 +204,8 @@ class BM25HybridSearchAlgorithm(SearchAlgorithm):
|
|||||||
results = []
|
results = []
|
||||||
|
|
||||||
for result in search_response.points:
|
for result in search_response.points:
|
||||||
|
if result.payload is None:
|
||||||
|
continue
|
||||||
# doc_id can be int (notes) or str (files - file paths)
|
# doc_id can be int (notes) or str (files - file paths)
|
||||||
doc_id = result.payload["doc_id"]
|
doc_id = result.payload["doc_id"]
|
||||||
doc_type = result.payload.get("doc_type", "note")
|
doc_type = result.payload.get("doc_type", "note")
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ class SemanticSearchAlgorithm(SearchAlgorithm):
|
|||||||
results = []
|
results = []
|
||||||
|
|
||||||
for result in search_response.points:
|
for result in search_response.points:
|
||||||
|
if result.payload is None:
|
||||||
|
continue
|
||||||
# doc_id can be int (notes) or str (files - file paths)
|
# doc_id can be int (notes) or str (files - file paths)
|
||||||
doc_id = result.payload["doc_id"]
|
doc_id = result.payload["doc_id"]
|
||||||
doc_type = result.payload.get("doc_type", "note")
|
doc_type = result.payload.get("doc_type", "note")
|
||||||
|
|||||||
@@ -206,7 +206,11 @@ async def scan_user_documents(
|
|||||||
limit=10000,
|
limit=10000,
|
||||||
)
|
)
|
||||||
|
|
||||||
indexed_doc_ids = {point.payload["doc_id"] for point in scroll_result[0]}
|
indexed_doc_ids = {
|
||||||
|
point.payload["doc_id"]
|
||||||
|
for point in (scroll_result[0] or [])
|
||||||
|
if point.payload is not None
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(f"Found {len(indexed_doc_ids)} indexed documents in Qdrant")
|
logger.debug(f"Found {len(indexed_doc_ids)} indexed documents in Qdrant")
|
||||||
|
|
||||||
@@ -376,7 +380,9 @@ async def scan_user_documents(
|
|||||||
)
|
)
|
||||||
|
|
||||||
indexed_file_ids = {
|
indexed_file_ids = {
|
||||||
point.payload["doc_id"] for point in file_scroll_result[0]
|
point.payload["doc_id"]
|
||||||
|
for point in (file_scroll_result[0] or [])
|
||||||
|
if point.payload is not None
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(f"Found {len(indexed_file_ids)} indexed files in Qdrant")
|
logger.debug(f"Found {len(indexed_file_ids)} indexed files in Qdrant")
|
||||||
@@ -611,7 +617,11 @@ async def scan_news_items(
|
|||||||
with_vectors=False,
|
with_vectors=False,
|
||||||
limit=10000,
|
limit=10000,
|
||||||
)
|
)
|
||||||
indexed_item_ids = {point.payload["doc_id"] for point in scroll_result[0]}
|
indexed_item_ids = {
|
||||||
|
point.payload["doc_id"]
|
||||||
|
for point in (scroll_result[0] or [])
|
||||||
|
if point.payload is not None
|
||||||
|
}
|
||||||
logger.debug(f"Found {len(indexed_item_ids)} indexed news items in Qdrant")
|
logger.debug(f"Found {len(indexed_item_ids)} indexed news items in Qdrant")
|
||||||
|
|
||||||
# Fetch all items (News app caps at ~200 per feed via auto-purge)
|
# Fetch all items (News app caps at ~200 per feed via auto-purge)
|
||||||
|
|||||||
@@ -189,25 +189,14 @@ async def test_get_file_info_returns_none_for_missing_file(mocker):
|
|||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
async def test_create_tag_creates_system_tag(mocker):
|
async def test_create_tag_creates_system_tag(mocker):
|
||||||
"""Test that create_tag creates a system tag via OCS API."""
|
"""Test that create_tag creates a system tag via WebDAV."""
|
||||||
mock_http_client = AsyncMock()
|
mock_http_client = AsyncMock()
|
||||||
client = WebDAVClient(mock_http_client, "testuser")
|
client = WebDAVClient(mock_http_client, "testuser")
|
||||||
|
|
||||||
# Mock OCS response
|
# Mock WebDAV response with Content-Location header
|
||||||
mock_response = AsyncMock()
|
mock_response = AsyncMock()
|
||||||
mock_response.status_code = 200
|
mock_response.status_code = 201
|
||||||
mock_response.json = mocker.Mock(
|
mock_response.headers = {"Content-Location": "/remote.php/dav/systemtags/42"}
|
||||||
return_value={
|
|
||||||
"ocs": {
|
|
||||||
"data": {
|
|
||||||
"id": 42,
|
|
||||||
"name": "vector-index",
|
|
||||||
"userVisible": True,
|
|
||||||
"userAssignable": True,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
mock_response.raise_for_status = mocker.Mock()
|
mock_response.raise_for_status = mocker.Mock()
|
||||||
|
|
||||||
mock_http_client.post = AsyncMock(return_value=mock_response)
|
mock_http_client.post = AsyncMock(return_value=mock_response)
|
||||||
@@ -224,8 +213,10 @@ async def test_create_tag_creates_system_tag(mocker):
|
|||||||
# Verify API call
|
# Verify API call
|
||||||
mock_http_client.post.assert_called_once()
|
mock_http_client.post.assert_called_once()
|
||||||
call_args = mock_http_client.post.call_args
|
call_args = mock_http_client.post.call_args
|
||||||
assert call_args[0][0] == "/ocs/v2.php/apps/systemtags/api/v1/tags"
|
assert call_args[0][0] == "/remote.php/dav/systemtags/"
|
||||||
assert call_args[1]["json"]["name"] == "vector-index"
|
assert call_args[1]["json"]["name"] == "vector-index"
|
||||||
|
assert call_args[1]["json"]["userVisible"] is True
|
||||||
|
assert call_args[1]["json"]["userAssignable"] is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|||||||
Reference in New Issue
Block a user