first commit

This commit is contained in:
2026-01-26 12:34:00 +01:00
commit e64465a7e6
29 changed files with 2952 additions and 0 deletions

156
src/config.py Normal file
View File

@@ -0,0 +1,156 @@
"""Configuration management for News Agent"""
import yaml
from pathlib import Path
from typing import Any
from pydantic import BaseModel, Field, HttpUrl
from pydantic_settings import BaseSettings, SettingsConfigDict
class RSSSource(BaseModel):
"""RSS feed source configuration"""
name: str
url: HttpUrl
category: str
class AIFilteringConfig(BaseModel):
"""AI filtering configuration"""
enabled: bool = True
min_score: float = Field(ge=0, le=10, default=6.5)
max_articles: int = Field(ge=1, default=15)
class AIConfig(BaseModel):
"""AI processing configuration"""
provider: str = "openrouter"
base_url: str = "https://openrouter.ai/api/v1"
model: str = "google/gemini-flash-1.5"
filtering: AIFilteringConfig
interests: list[str]
class SMTPConfig(BaseModel):
"""SMTP server configuration"""
host: str = "localhost"
port: int = 25
use_tls: bool = False
use_ssl: bool = False
username: str | None = None
password: str | None = None
class EmailConfig(BaseModel):
"""Email configuration"""
to: str
from_: str = Field(alias="from")
from_name: str = "Daily Tech News Agent"
subject_template: str = "Tech News Digest - {date}"
smtp: SMTPConfig
class ScheduleConfig(BaseModel):
"""Schedule configuration"""
time: str = "07:00"
timezone: str = "Europe/Oslo"
class DatabaseConfig(BaseModel):
"""Database configuration"""
path: str = "data/articles.db"
retention_days: int = 30
class LoggingConfig(BaseModel):
"""Logging configuration"""
level: str = "INFO"
file: str = "data/logs/news-agent.log"
max_bytes: int = 10485760
backup_count: int = 5
class EnvSettings(BaseSettings):
"""Environment variable settings"""
model_config = SettingsConfigDict(env_file=".env", case_sensitive=False, extra="ignore")
openrouter_api_key: str
openrouter_site_url: str | None = None
openrouter_site_name: str | None = None
smtp_username: str | None = None
smtp_password: str | None = None
error_notification_email: str | None = None
class Config:
"""Main configuration manager"""
def __init__(self, config_path: str | Path = "config.yaml"):
"""Load configuration from YAML file and environment variables"""
self.config_path = Path(config_path)
# Load YAML configuration
with open(self.config_path) as f:
self._config: dict[str, Any] = yaml.safe_load(f)
# Load environment variables
self.env = EnvSettings()
@property
def rss_sources(self) -> list[RSSSource]:
"""Get all RSS sources"""
return [RSSSource(**src) for src in self._config["sources"]["rss"]]
@property
def ai(self) -> AIConfig:
"""Get AI configuration"""
return AIConfig(**self._config["ai"])
@property
def email(self) -> EmailConfig:
"""Get email configuration"""
email_config = self._config["email"].copy()
# Merge SMTP credentials from environment variables
if self.env.smtp_username:
email_config["smtp"]["username"] = self.env.smtp_username
if self.env.smtp_password:
email_config["smtp"]["password"] = self.env.smtp_password
return EmailConfig(**email_config)
@property
def schedule(self) -> ScheduleConfig:
"""Get schedule configuration"""
return ScheduleConfig(**self._config["schedule"])
@property
def database(self) -> DatabaseConfig:
"""Get database configuration"""
return DatabaseConfig(**self._config["database"])
@property
def logging(self) -> LoggingConfig:
"""Get logging configuration"""
return LoggingConfig(**self._config["logging"])
# Global config instance (lazy loaded)
_config: Config | None = None
def get_config() -> Config:
"""Get or create global config instance"""
global _config
if _config is None:
_config = Config()
return _config