fix: auto-fix ruff linting errors and remove unsupported upload-artifact
All checks were successful
CI / Lint (push) Successful in 52s
CI / Test (push) Successful in 1m1s
CI / Release (push) Successful in 5s
CI / Notify (push) Successful in 1s

This commit is contained in:
2026-02-02 08:34:00 -05:00
parent 7b30ff6a05
commit dbf1a93141
19 changed files with 414 additions and 400 deletions

View File

@@ -57,12 +57,6 @@ jobs:
- name: Run tests with coverage
run: uv run pytest --cov=handler_base --cov-report=xml --cov-report=term
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage.xml
release:
name: Release
runs-on: ubuntu-latest

View File

@@ -8,11 +8,12 @@ Provides consistent patterns for:
- Graceful shutdown
- Service client wrappers
"""
from handler_base.config import Settings
from handler_base.handler import Handler
from handler_base.health import HealthServer
from handler_base.nats_client import NATSClient
from handler_base.telemetry import setup_telemetry, get_tracer, get_meter
from handler_base.telemetry import get_meter, get_tracer, setup_telemetry
__all__ = [
"Handler",

View File

@@ -1,12 +1,13 @@
"""
Service client wrappers for AI/ML backends.
"""
from handler_base.clients.embeddings import EmbeddingsClient
from handler_base.clients.reranker import RerankerClient
from handler_base.clients.llm import LLMClient
from handler_base.clients.tts import TTSClient
from handler_base.clients.stt import STTClient
from handler_base.clients.milvus import MilvusClient
from handler_base.clients.reranker import RerankerClient
from handler_base.clients.stt import STTClient
from handler_base.clients.tts import TTSClient
__all__ = [
"EmbeddingsClient",

View File

@@ -1,6 +1,7 @@
"""
Embeddings service client (Infinity/BGE).
"""
import logging
from typing import Optional

View File

@@ -1,8 +1,9 @@
"""
LLM service client (vLLM/OpenAI-compatible).
"""
import logging
from typing import Optional, AsyncIterator
from typing import AsyncIterator, Optional
import httpx
@@ -131,9 +132,7 @@ class LLMClient:
"stream": True,
}
async with self._client.stream(
"POST", "/v1/chat/completions", json=payload
) as response:
async with self._client.stream("POST", "/v1/chat/completions", json=payload) as response:
response.raise_for_status()
async for line in response.aiter_lines():
@@ -143,6 +142,7 @@ class LLMClient:
break
import json
chunk = json.loads(data)
delta = chunk["choices"][0].get("delta", {})
content = delta.get("content", "")
@@ -163,21 +163,25 @@ class LLMClient:
messages.append({"role": "system", "content": system_prompt})
elif context:
# Default RAG system prompt
messages.append({
"role": "system",
"content": (
"You are a helpful assistant. Use the provided context to answer "
"the user's question. If the context doesn't contain relevant "
"information, say so."
),
})
messages.append(
{
"role": "system",
"content": (
"You are a helpful assistant. Use the provided context to answer "
"the user's question. If the context doesn't contain relevant "
"information, say so."
),
}
)
# Add context as a separate message if provided
if context:
messages.append({
"role": "user",
"content": f"Context:\n{context}\n\nQuestion: {prompt}",
})
messages.append(
{
"role": "user",
"content": f"Context:\n{context}\n\nQuestion: {prompt}",
}
)
else:
messages.append({"role": "user", "content": prompt})

View File

@@ -1,10 +1,11 @@
"""
Milvus vector database client.
"""
import logging
from typing import Optional, Any
from pymilvus import connections, Collection, utility
import logging
from typing import Optional
from pymilvus import Collection, connections, utility
from handler_base.config import Settings
from handler_base.telemetry import create_span

View File

@@ -1,6 +1,7 @@
"""
Reranker service client (Infinity/BGE Reranker).
"""
import logging
from typing import Optional
@@ -73,11 +74,13 @@ class RerankerClient:
enriched = []
for r in results:
idx = r.get("index", 0)
enriched.append({
"index": idx,
"score": r.get("relevance_score", r.get("score", 0)),
"document": documents[idx] if idx < len(documents) else "",
})
enriched.append(
{
"index": idx,
"score": r.get("relevance_score", r.get("score", 0)),
"document": documents[idx] if idx < len(documents) else "",
}
)
return enriched

View File

@@ -1,7 +1,7 @@
"""
STT service client (Whisper/faster-whisper).
"""
import io
import logging
from typing import Optional

View File

@@ -1,7 +1,7 @@
"""
TTS service client (Coqui XTTS).
"""
import io
import logging
from typing import Optional

View File

@@ -3,7 +3,9 @@ Configuration management using Pydantic Settings.
Environment variables are automatically loaded and validated.
"""
from typing import Optional
from pydantic_settings import BaseSettings, SettingsConfigDict

View File

@@ -1,6 +1,7 @@
"""
Base handler class for building NATS-based services.
"""
import asyncio
import logging
import signal
@@ -12,7 +13,7 @@ from nats.aio.msg import Msg
from handler_base.config import Settings
from handler_base.health import HealthServer
from handler_base.nats_client import NATSClient
from handler_base.telemetry import setup_telemetry, create_span
from handler_base.telemetry import create_span, setup_telemetry
logger = logging.getLogger(__name__)
@@ -178,7 +179,7 @@ class Handler(ABC):
# Wait for shutdown signal
await self._shutdown_event.wait()
except Exception as e:
except Exception:
logger.exception("Fatal error in handler")
raise
finally:

View File

@@ -3,12 +3,13 @@ HTTP health check server.
Provides /health and /ready endpoints for Kubernetes probes.
"""
import asyncio
import logging
from typing import Callable, Optional, Awaitable
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
import json
import logging
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Awaitable, Callable, Optional
from handler_base.config import Settings

View File

@@ -1,9 +1,9 @@
"""
NATS client wrapper with connection management and utilities.
"""
import asyncio
import logging
from typing import Any, Callable, Optional, Awaitable
from typing import Any, Awaitable, Callable, Optional
import msgpack
import nats
@@ -129,6 +129,7 @@ class NATSClient:
payload = msgpack.packb(data, use_bin_type=True)
else:
import json
payload = json.dumps(data).encode()
await self.nc.publish(subject, payload)
@@ -162,6 +163,7 @@ class NATSClient:
payload = msgpack.packb(data, use_bin_type=True)
else:
import json
payload = json.dumps(data).encode()
response = await self.nc.request(subject, payload, timeout=timeout)
@@ -170,6 +172,7 @@ class NATSClient:
return msgpack.unpackb(response.data, raw=False)
else:
import json
return json.loads(response.data.decode())
@staticmethod
@@ -181,4 +184,5 @@ class NATSClient:
def decode_json(msg: Msg) -> Any:
"""Decode a JSON message."""
import json
return json.loads(msg.data.decode())

View File

@@ -3,26 +3,27 @@ OpenTelemetry setup for tracing and metrics.
Supports both gRPC and HTTP exporters, with optional HyperDX integration.
"""
import logging
import os
from typing import Optional, Tuple
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry import metrics, trace
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
OTLPSpanExporter as OTLPSpanExporterHTTP,
)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
OTLPMetricExporter as OTLPMetricExporterHTTP,
)
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION, SERVICE_NAMESPACE
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
OTLPSpanExporter as OTLPSpanExporterHTTP,
)
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import SERVICE_NAME, SERVICE_NAMESPACE, SERVICE_VERSION, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from handler_base.config import Settings
@@ -60,13 +61,15 @@ def setup_telemetry(
return None, None
# Create resource with service information
resource = Resource.create({
SERVICE_NAME: settings.service_name,
SERVICE_VERSION: settings.service_version,
SERVICE_NAMESPACE: settings.service_namespace,
"deployment.environment": settings.deployment_env,
"host.name": os.environ.get("HOSTNAME", "unknown"),
})
resource = Resource.create(
{
SERVICE_NAME: settings.service_name,
SERVICE_VERSION: settings.service_version,
SERVICE_NAMESPACE: settings.service_namespace,
"deployment.environment": settings.deployment_env,
"host.name": os.environ.get("HOSTNAME", "unknown"),
}
)
# Determine endpoint and exporter type
if settings.hyperdx_enabled and settings.hyperdx_api_key:
@@ -150,5 +153,6 @@ def create_span(name: str, **kwargs):
if _tracer is None:
# Return a no-op context manager
from contextlib import nullcontext
return nullcontext()
return _tracer.start_as_current_span(name, **kwargs)

View File

@@ -1,14 +1,13 @@
"""
Pytest configuration and fixtures.
"""
import asyncio
import os
from typing import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock
import pytest
# Set test environment variables before importing handler_base
os.environ.setdefault("NATS_URL", "nats://localhost:4222")
os.environ.setdefault("REDIS_URL", "redis://localhost:6379")
@@ -29,6 +28,7 @@ def event_loop():
def settings():
"""Create test settings."""
from handler_base.config import Settings
return Settings(
service_name="test-service",
service_version="1.0.0-test",
@@ -56,7 +56,7 @@ def mock_nats_message():
msg = MagicMock()
msg.subject = "test.subject"
msg.reply = "test.reply"
msg.data = b'\x82\xa8query\xa5hello\xaarequest_id\xa4test' # msgpack
msg.data = b"\x82\xa8query\xa5hello\xaarequest_id\xa4test" # msgpack
return msg

View File

@@ -1,9 +1,10 @@
"""
Unit tests for service clients.
"""
import json
from unittest.mock import MagicMock
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
class TestEmbeddingsClient:
@@ -23,9 +24,7 @@ class TestEmbeddingsClient:
"""Test embedding a single text."""
# Setup mock response
mock_response = MagicMock()
mock_response.json.return_value = {
"data": [{"embedding": sample_embedding, "index": 0}]
}
mock_response.json.return_value = {"data": [{"embedding": sample_embedding, "index": 0}]}
mock_response.raise_for_status = MagicMock()
mock_httpx_client.post.return_value = mock_response
@@ -118,10 +117,8 @@ class TestLLMClient:
"""Test generating a response."""
mock_response = MagicMock()
mock_response.json.return_value = {
"choices": [
{"message": {"content": "Hello! I'm an AI assistant."}}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 20}
"choices": [{"message": {"content": "Hello! I'm an AI assistant."}}],
"usage": {"prompt_tokens": 10, "completion_tokens": 20},
}
mock_response.raise_for_status = MagicMock()
mock_httpx_client.post.return_value = mock_response
@@ -135,17 +132,14 @@ class TestLLMClient:
"""Test generating with RAG context."""
mock_response = MagicMock()
mock_response.json.return_value = {
"choices": [
{"message": {"content": "Based on the context..."}}
],
"usage": {}
"choices": [{"message": {"content": "Based on the context..."}}],
"usage": {},
}
mock_response.raise_for_status = MagicMock()
mock_httpx_client.post.return_value = mock_response
result = await llm_client.generate(
"What is Python?",
context="Python is a programming language."
"What is Python?", context="Python is a programming language."
)
assert "Based on the context" in result

View File

@@ -1,8 +1,6 @@
"""
Unit tests for handler_base.config module.
"""
import os
import pytest
class TestSettings:
@@ -22,6 +20,7 @@ class TestSettings:
# Need to reimport to pick up env changes
from handler_base.config import Settings
s = Settings()
assert s.service_name == "env-service"

View File

@@ -1,12 +1,12 @@
"""
Unit tests for handler_base.health module.
"""
import pytest
import json
import threading
import time
from http.client import HTTPConnection
from unittest.mock import AsyncMock
import pytest
class TestHealthServer:

View File

@@ -1,9 +1,11 @@
"""
Unit tests for handler_base.nats_client module.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
import msgpack
import pytest
class TestNATSClient:
@@ -13,6 +15,7 @@ class TestNATSClient:
def nats_client(self, settings):
"""Create a NATSClient instance."""
from handler_base.nats_client import NATSClient
return NATSClient(settings)
def test_init(self, nats_client, settings):
@@ -35,6 +38,7 @@ class TestNATSClient:
def test_decode_json(self, nats_client):
"""Test JSON decoding."""
import json
data = {"query": "hello"}
msg = MagicMock()