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 - name: Run tests with coverage
run: uv run pytest --cov=handler_base --cov-report=xml --cov-report=term 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: release:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -8,11 +8,12 @@ Provides consistent patterns for:
- Graceful shutdown - Graceful shutdown
- Service client wrappers - Service client wrappers
""" """
from handler_base.config import Settings from handler_base.config import Settings
from handler_base.handler import Handler from handler_base.handler import Handler
from handler_base.health import HealthServer from handler_base.health import HealthServer
from handler_base.nats_client import NATSClient 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__ = [ __all__ = [
"Handler", "Handler",

View File

@@ -1,12 +1,13 @@
""" """
Service client wrappers for AI/ML backends. Service client wrappers for AI/ML backends.
""" """
from handler_base.clients.embeddings import EmbeddingsClient 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.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.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__ = [ __all__ = [
"EmbeddingsClient", "EmbeddingsClient",

View File

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

View File

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

View File

@@ -1,10 +1,11 @@
""" """
Milvus vector database client. 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.config import Settings
from handler_base.telemetry import create_span from handler_base.telemetry import create_span

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
""" """
NATS client wrapper with connection management and utilities. NATS client wrapper with connection management and utilities.
""" """
import asyncio
import logging import logging
from typing import Any, Callable, Optional, Awaitable from typing import Any, Awaitable, Callable, Optional
import msgpack import msgpack
import nats import nats
@@ -129,6 +129,7 @@ class NATSClient:
payload = msgpack.packb(data, use_bin_type=True) payload = msgpack.packb(data, use_bin_type=True)
else: else:
import json import json
payload = json.dumps(data).encode() payload = json.dumps(data).encode()
await self.nc.publish(subject, payload) await self.nc.publish(subject, payload)
@@ -162,6 +163,7 @@ class NATSClient:
payload = msgpack.packb(data, use_bin_type=True) payload = msgpack.packb(data, use_bin_type=True)
else: else:
import json import json
payload = json.dumps(data).encode() payload = json.dumps(data).encode()
response = await self.nc.request(subject, payload, timeout=timeout) response = await self.nc.request(subject, payload, timeout=timeout)
@@ -170,6 +172,7 @@ class NATSClient:
return msgpack.unpackb(response.data, raw=False) return msgpack.unpackb(response.data, raw=False)
else: else:
import json import json
return json.loads(response.data.decode()) return json.loads(response.data.decode())
@staticmethod @staticmethod
@@ -181,4 +184,5 @@ class NATSClient:
def decode_json(msg: Msg) -> Any: def decode_json(msg: Msg) -> Any:
"""Decode a JSON message.""" """Decode a JSON message."""
import json import json
return json.loads(msg.data.decode()) 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. Supports both gRPC and HTTP exporters, with optional HyperDX integration.
""" """
import logging import logging
import os import os
from typing import Optional, Tuple from typing import Optional, Tuple
from opentelemetry import trace, metrics from opentelemetry import metrics, trace
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.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
OTLPSpanExporter as OTLPSpanExporterHTTP,
)
from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
OTLPMetricExporter as OTLPMetricExporterHTTP, 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.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor 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 from handler_base.config import Settings
@@ -60,13 +61,15 @@ def setup_telemetry(
return None, None return None, None
# Create resource with service information # Create resource with service information
resource = Resource.create({ resource = Resource.create(
{
SERVICE_NAME: settings.service_name, SERVICE_NAME: settings.service_name,
SERVICE_VERSION: settings.service_version, SERVICE_VERSION: settings.service_version,
SERVICE_NAMESPACE: settings.service_namespace, SERVICE_NAMESPACE: settings.service_namespace,
"deployment.environment": settings.deployment_env, "deployment.environment": settings.deployment_env,
"host.name": os.environ.get("HOSTNAME", "unknown"), "host.name": os.environ.get("HOSTNAME", "unknown"),
}) }
)
# Determine endpoint and exporter type # Determine endpoint and exporter type
if settings.hyperdx_enabled and settings.hyperdx_api_key: if settings.hyperdx_enabled and settings.hyperdx_api_key:
@@ -150,5 +153,6 @@ def create_span(name: str, **kwargs):
if _tracer is None: if _tracer is None:
# Return a no-op context manager # Return a no-op context manager
from contextlib import nullcontext from contextlib import nullcontext
return nullcontext() return nullcontext()
return _tracer.start_as_current_span(name, **kwargs) return _tracer.start_as_current_span(name, **kwargs)

View File

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

View File

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

View File

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

View File

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

View File

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