refactor: align backend layout and supabase infra

Consolidate backend modules/tests under the backend package while syncing Supabase compose/env config and related plans.
This commit is contained in:
qzl
2026-02-05 15:13:06 +08:00
parent 3cfcb11240
commit ad06fe7de4
111 changed files with 5540 additions and 1362 deletions
+1
View File
@@ -0,0 +1 @@
from __future__ import annotations
+21
View File
@@ -0,0 +1,21 @@
from __future__ import annotations
from services.base.qdrant import QdrantService, qdrant_service
from services.base.redis import RedisService, redis_service
from services.base.service_interface import (
BaseServiceProvider,
ServiceRegistry,
register_service,
register_service_instance,
)
__all__ = [
"BaseServiceProvider",
"QdrantService",
"RedisService",
"ServiceRegistry",
"qdrant_service",
"redis_service",
"register_service",
"register_service_instance",
]
+94
View File
@@ -0,0 +1,94 @@
from __future__ import annotations
import asyncio
from typing import Any, Dict, Optional
from qdrant_client import QdrantClient
from core.config.settings import QdrantSettings, config
from .service_interface import BaseServiceProvider, register_service_instance
class QdrantService(BaseServiceProvider):
def __init__(self, settings: QdrantSettings | None = None) -> None:
super().__init__("qdrant")
self._settings = settings or config.qdrant
self._client: Optional[QdrantClient] = None
def _build_client(self) -> QdrantClient:
return QdrantClient(
url=self._settings.url,
api_key=self._settings.api_key,
timeout=self._settings.timeout,
prefer_grpc=self._settings.prefer_grpc,
)
def _require_client(self) -> QdrantClient:
client = self._client
if client is None:
raise RuntimeError("Qdrant client is not initialized")
return client
async def initialize(self, **_: Any) -> bool:
try:
client = self._build_client()
collections = await asyncio.to_thread(client.get_collections)
self.logger.info(
"Qdrant service initialized",
collections_count=len(collections.collections),
)
self._client = client
self._set_initialized(True)
return True
except Exception as exc: # noqa: BLE001
self.logger.warning("Qdrant service initialization failed", error=str(exc))
self._client = None
self._set_initialized(False)
return False
async def close(self) -> bool:
client = self._client
if client is None:
return True
try:
close = getattr(client, "close", None)
if callable(close):
await asyncio.to_thread(close)
self.logger.info("Qdrant service closed")
self._client = None
self._set_initialized(False)
return True
except Exception as exc: # noqa: BLE001
self.logger.exception("Qdrant service close failed", error=str(exc))
self._client = None
self._set_initialized(False)
return False
async def health_check(self) -> Dict[str, Any]:
client = self._client
if client is None:
return {"status": "unhealthy", "details": {"error": "not initialized"}}
try:
collections = await asyncio.to_thread(client.get_collections)
return {
"status": "healthy",
"details": {
"connected": True,
"collections_count": len(collections.collections),
"collections": [
collection.name for collection in collections.collections[:5]
],
},
}
except Exception as exc: # noqa: BLE001
self.logger.warning("Qdrant health check failed", error=str(exc))
return {"status": "unhealthy", "details": {"error": str(exc)}}
def get_client(self) -> QdrantClient:
return self._require_client()
qdrant_service: QdrantService = register_service_instance("qdrant", QdrantService())
__all__ = ["QdrantService", "qdrant_service"]
+97
View File
@@ -0,0 +1,97 @@
from __future__ import annotations
import inspect
from typing import Any, Dict, Optional
import redis.asyncio as redis
from core.config.settings import RedisSettings, config
from .service_interface import BaseServiceProvider, register_service_instance
class RedisService(BaseServiceProvider):
def __init__(self, settings: RedisSettings | None = None) -> None:
super().__init__("redis")
self._settings = settings or config.redis
self._client: Optional[redis.Redis] = None
def _build_client(self) -> redis.Redis:
return redis.from_url(
self._settings.url,
decode_responses=True,
socket_connect_timeout=self._settings.socket_connect_timeout,
socket_timeout=self._settings.socket_timeout,
max_connections=self._settings.max_connections,
)
def _require_client(self) -> redis.Redis:
client = self._client
if client is None:
raise RuntimeError("Redis client is not initialized")
return client
async def initialize(self, **_: Any) -> bool:
try:
client = self._build_client()
ping_result = client.ping()
if inspect.isawaitable(ping_result):
await ping_result
self._client = client
self._set_initialized(True)
self.logger.info("Redis service initialized")
return True
except Exception as exc: # noqa: BLE001
self.logger.warning("Redis service initialization failed", error=str(exc))
self._client = None
self._set_initialized(False)
return False
async def close(self) -> bool:
client = self._client
if client is None:
return True
try:
await client.aclose()
self.logger.info("Redis service closed")
self._client = None
self._set_initialized(False)
return True
except Exception as exc: # noqa: BLE001
self.logger.exception("Redis service close failed", error=str(exc))
return False
async def health_check(self) -> Dict[str, Any]:
client = self._client
if client is None:
return {"status": "unhealthy", "details": {"error": "not initialized"}}
try:
ping_result = client.ping()
ping = (
await ping_result if inspect.isawaitable(ping_result) else ping_result
)
info_result = client.info()
info = (
await info_result if inspect.isawaitable(info_result) else info_result
)
return {
"status": "healthy" if ping else "unhealthy",
"details": {
"ping": ping,
"redis_version": info.get("redis_version"),
"connected_clients": info.get("connected_clients"),
"used_memory": info.get("used_memory_human"),
"uptime_in_seconds": info.get("uptime_in_seconds"),
},
}
except Exception as exc: # noqa: BLE001
self.logger.warning("Redis health check failed", error=str(exc))
return {"status": "unhealthy", "details": {"error": str(exc)}}
def get_client(self) -> redis.Redis:
return self._require_client()
redis_service: RedisService = register_service_instance("redis", RedisService())
__all__ = ["RedisService", "redis_service"]
@@ -0,0 +1,84 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Callable, Dict, Optional, TypeVar
from core.logging import get_logger
class BaseServiceProvider(ABC):
def __init__(self, service_name: str) -> None:
self.service_name = service_name
self._initialized = False
self.logger = get_logger("services.base").bind(service=service_name)
@abstractmethod
async def initialize(self, **kwargs: Any) -> bool:
raise NotImplementedError
@abstractmethod
async def close(self) -> bool:
raise NotImplementedError
@abstractmethod
async def health_check(self) -> Dict[str, Any]:
raise NotImplementedError
@property
def is_initialized(self) -> bool:
return self._initialized
def _set_initialized(self, value: bool) -> None:
self._initialized = value
def get_service_info(self) -> Dict[str, Any]:
return {
"name": self.service_name,
"initialized": self._initialized,
"type": self.__class__.__name__,
}
class ServiceRegistry:
_services: Dict[str, Callable[..., BaseServiceProvider]] = {}
@classmethod
def register(
cls, service_name: str, factory: Callable[..., BaseServiceProvider]
) -> None:
cls._services = {**cls._services, service_name: factory}
@classmethod
def get_service_factory(
cls, service_name: str
) -> Optional[Callable[..., BaseServiceProvider]]:
return cls._services.get(service_name)
@classmethod
def list_services(cls) -> list[str]:
return sorted(cls._services.keys())
@classmethod
def create_service(
cls, service_name: str, **kwargs: Any
) -> Optional[BaseServiceProvider]:
factory = cls.get_service_factory(service_name)
if not factory:
return None
return factory(**kwargs)
def register_service(service_name: str) -> Callable[[type], type]:
def decorator(service_class: type) -> type:
ServiceRegistry.register(service_name, service_class)
return service_class
return decorator
TService = TypeVar("TService", bound=BaseServiceProvider)
def register_service_instance(service_name: str, service: TService) -> TService:
ServiceRegistry.register(service_name, lambda: service)
return service