feat: scaffold Focus export logging skeleton

This commit is contained in:
Yuta Saito 2026-01-08 12:09:58 +09:00
parent 9dd9f9fc43
commit 565a622cf9
11 changed files with 299 additions and 0 deletions

View file

View file

@ -0,0 +1,22 @@
"""Database access helpers for Focus export."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
import polars as pl
class FocusLiteLLMDatabase:
"""Retrieves LiteLLM usage data for Focus export workflows."""
async def get_usage_data(
self,
*,
limit: Optional[int] = None,
start_time_utc: Optional[datetime] = None,
end_time_utc: Optional[datetime] = None,
) -> pl.DataFrame:
"""Return usage data for the requested window."""
raise NotImplementedError

View file

@ -0,0 +1,12 @@
"""Destination implementations for Focus export."""
from .base import FocusDestination, FocusTimeWindow
from .factory import FocusDestinationFactory
from .s3_destination import FocusS3Destination
__all__ = [
"FocusDestination",
"FocusDestinationFactory",
"FocusTimeWindow",
"FocusS3Destination",
]

View file

@ -0,0 +1,30 @@
"""Abstract destination interfaces for Focus export."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import Protocol
@dataclass(frozen=True)
class FocusTimeWindow:
"""Represents the span of data exported in a single batch."""
start_time: datetime
end_time: datetime
frequency: str
class FocusDestination(Protocol):
"""Protocol for anything that can receive Focus export files."""
async def deliver(
self,
*,
content: bytes,
time_window: FocusTimeWindow,
filename: str,
) -> None:
"""Persist the serialized export for the provided time window."""
...

View file

@ -0,0 +1,21 @@
"""Factory helpers for Focus export destinations."""
from __future__ import annotations
from typing import Optional
from .base import FocusDestination
class FocusDestinationFactory:
"""Builds destination instances based on provider/config settings."""
@staticmethod
def create(
*,
provider: str,
prefix: str,
config: Optional[dict] = None,
) -> FocusDestination:
"""Return a destination implementation for the requested provider."""
raise NotImplementedError

View file

@ -0,0 +1,32 @@
"""S3 destination implementation for Focus export."""
from __future__ import annotations
from typing import Any, Optional
from .base import FocusDestination, FocusTimeWindow
class FocusS3Destination(FocusDestination):
"""Handles uploading serialized exports to S3 buckets."""
def __init__(
self,
*,
prefix: str,
config: Optional[dict[str, Any]] = None,
) -> None:
self.prefix = prefix.rstrip("/")
self.config = config or {}
async def deliver(
self,
*,
content: bytes,
time_window: FocusTimeWindow,
filename: str,
) -> None:
raise NotImplementedError
def _build_object_key(self, *, time_window: FocusTimeWindow, filename: str) -> str:
raise NotImplementedError

View file

@ -0,0 +1,129 @@
"""Focus export logger orchestrating DB pull/transform/upload."""
from __future__ import annotations
import os
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Any, Optional
import polars as pl
from litellm.integrations.custom_logger import CustomLogger
from .destinations import (
FocusDestination,
FocusDestinationFactory,
FocusTimeWindow,
)
from .serializers import FocusParquetSerializer, FocusSerializer
from .transformer import FocusTransformer
if TYPE_CHECKING:
from apscheduler.schedulers.asyncio import AsyncIOScheduler
else:
AsyncIOScheduler = Any
class FocusExportLogger(CustomLogger):
"""Coordinates Focus export jobs across transformer/serializer/destination layers."""
def __init__(
self,
*,
provider: Optional[str] = None,
export_format: Optional[str] = None,
frequency: Optional[str] = None,
cron_offset_minute: Optional[int] = None,
interval_seconds: Optional[int] = None,
prefix: Optional[str] = None,
destination_config: Optional[dict[str, Any]] = None,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.provider = (provider or os.getenv("FOCUS_EXPORT_PROVIDER") or "s3").lower()
self.export_format = (
export_format or os.getenv("FOCUS_EXPORT_FORMAT") or "parquet"
).lower()
self.frequency = (
frequency or os.getenv("FOCUS_EXPORT_FREQUENCY") or "hourly"
).lower()
self.cron_offset_minute = (
cron_offset_minute
if cron_offset_minute is not None
else int(os.getenv("FOCUS_EXPORT_CRON_OFFSET", "5"))
)
self.interval_seconds = (
interval_seconds
if interval_seconds is not None
else os.getenv("FOCUS_EXPORT_INTERVAL_SECONDS")
)
self.prefix = prefix or os.getenv("FOCUS_EXPORT_PREFIX", "focus_exports")
self._destination = self._init_destination(
destination_config=destination_config,
)
self._serializer = self._init_serializer()
self._transformer = FocusTransformer()
def _init_serializer(self) -> FocusSerializer:
"""Return serializer implementation for requested format."""
if self.export_format != "parquet":
raise NotImplementedError("Only parquet export supported currently")
return FocusParquetSerializer()
def _init_destination(
self,
*,
destination_config: Optional[dict[str, Any]],
) -> FocusDestination:
"""Factory for destination implementations."""
resolved_config = self._resolve_destination_config(destination_config)
return FocusDestinationFactory.create(
provider=self.provider,
prefix=self.prefix,
config=resolved_config,
)
def _resolve_destination_config(
self,
destination_config: Optional[dict[str, Any]],
) -> dict[str, Any]:
"""Collect provider-specific configuration for destination creation."""
raise NotImplementedError
async def export_usage_data(self) -> None:
"""Public hook to trigger export immediately."""
raise NotImplementedError
async def dry_run_export_usage_data(self) -> dict:
"""Return transformed data without uploading."""
raise NotImplementedError
async def initialize_focus_export_job(self) -> None:
"""Entry point for scheduler jobs to run export cycle with locking."""
raise NotImplementedError
@staticmethod
async def init_focus_export_background_job(
scheduler: AsyncIOScheduler,
) -> None:
"""Register the export cron/interval job with the provided scheduler."""
raise NotImplementedError
def _compute_time_window(self, now: datetime) -> FocusTimeWindow:
"""Derive the time window to export based on configured frequency."""
raise NotImplementedError
def _serialize_and_upload(
self,
frame: pl.DataFrame,
window: FocusTimeWindow,
) -> None:
"""Helper stub for serializing and delegating to destination."""
raise NotImplementedError
def _build_filename(self) -> str:
"""Return the canonical file name for exports."""
if not self._serializer.extension:
raise ValueError("Serializer must declare a file extension")
return f"usage.{self._serializer.extension}"

View file

@ -0,0 +1,6 @@
"""Serializer package exports for Focus integration."""
from .base import FocusSerializer
from .parquet import FocusParquetSerializer
__all__ = ["FocusSerializer", "FocusParquetSerializer"]

View file

@ -0,0 +1,18 @@
"""Serializer abstractions for Focus export."""
from __future__ import annotations
from abc import ABC, abstractmethod
import polars as pl
class FocusSerializer(ABC):
"""Base serializer turning Focus frames into bytes."""
extension: str = ""
@abstractmethod
def serialize(self, frame: pl.DataFrame) -> bytes:
"""Convert the normalized Focus frame into the chosen format."""
raise NotImplementedError

View file

@ -0,0 +1,16 @@
"""Parquet serializer for Focus export."""
from __future__ import annotations
import polars as pl
from .base import FocusSerializer
class FocusParquetSerializer(FocusSerializer):
"""Placeholder Parquet serializer implementation."""
extension = "parquet"
def serialize(self, frame: pl.DataFrame) -> bytes:
raise NotImplementedError

View file

@ -0,0 +1,13 @@
"""Focus export data transformer."""
from __future__ import annotations
import polars as pl
class FocusTransformer:
"""Transforms LiteLLM DB rows into Focus-compatible schema."""
def transform(self, frame: pl.DataFrame) -> pl.DataFrame:
"""Return a normalized frame expected by downstream serializers."""
raise NotImplementedError