""" Database service Implements project mapping, branch pattern matching, and related features """ import asyncio from typing import Optional, List, Dict, Any from datetime import datetime import structlog import re from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, ForeignKey from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.sql import text from app.config import get_settings logger = structlog.get_logger() Base = declarative_base() # Database models class APIKey(Base): """API key model""" __tablename__ = "api_keys" id = Column(Integer, primary_key=True, autoincrement=True) key = Column(String(255), unique=True, nullable=False) description = Column(Text) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class ProjectMapping(Base): """Project mapping model""" __tablename__ = "project_mappings" id = Column(Integer, primary_key=True, autoincrement=True) repository_name = Column(String(255), unique=True, nullable=False) default_job = Column(String(255)) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationships branch_jobs = relationship("BranchJob", back_populates="project", cascade="all, delete-orphan") branch_patterns = relationship("BranchPattern", back_populates="project", cascade="all, delete-orphan") class BranchJob(Base): """Branch job mapping model""" __tablename__ = "branch_jobs" id = Column(Integer, primary_key=True, autoincrement=True) project_id = Column(Integer, ForeignKey("project_mappings.id"), nullable=False) branch_name = Column(String(255), nullable=False) job_name = Column(String(255), nullable=False) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationship project = relationship("ProjectMapping", back_populates="branch_jobs") class BranchPattern(Base): """Branch pattern mapping model""" __tablename__ = "branch_patterns" id = Column(Integer, primary_key=True, autoincrement=True) project_id = Column(Integer, ForeignKey("project_mappings.id"), nullable=False) pattern = Column(String(255), nullable=False) job_name = Column(String(255), nullable=False) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationship project = relationship("ProjectMapping", back_populates="branch_patterns") class TriggerLog(Base): """Trigger log model""" __tablename__ = "trigger_logs" id = Column(Integer, primary_key=True, autoincrement=True) repository_name = Column(String(255), nullable=False) branch_name = Column(String(255), nullable=False) commit_sha = Column(String(255), nullable=False) job_name = Column(String(255), nullable=False) status = Column(String(50), nullable=False) error_message = Column(Text) created_at = Column(DateTime, default=datetime.utcnow) class DatabaseService: """Database service""" def __init__(self): self.settings = get_settings() self.engine = None self.SessionLocal = None self._init_database() def _init_database(self): """Initialize database""" try: self.engine = create_engine( self.settings.database.url, echo=self.settings.database.echo, pool_size=self.settings.database.pool_size, max_overflow=self.settings.database.max_overflow ) # Create tables Base.metadata.create_all(bind=self.engine) # Create session factory self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine) logger.info("Database initialized successfully") except Exception as e: logger.error("Failed to initialize database", error=str(e)) raise def get_session(self): """Get database session""" return self.SessionLocal() async def get_project_mapping(self, repository_name: str) -> Optional[Dict[str, Any]]: """ Get project mapping Args: repository_name: repository name Returns: Dict: project mapping info """ try: def _get_mapping(): session = self.get_session() try: project = session.query(ProjectMapping).filter( ProjectMapping.repository_name == repository_name ).first() if not project: return None # Build return data result = { "id": project.id, "repository_name": project.repository_name, "default_job": project.default_job, "branch_jobs": [], "branch_patterns": [] } # Add branch job mappings for branch_job in project.branch_jobs: result["branch_jobs"].append({ "id": branch_job.id, "branch_name": branch_job.branch_name, "job_name": branch_job.job_name }) # Add branch pattern mappings for pattern in project.branch_patterns: result["branch_patterns"].append({ "id": pattern.id, "pattern": pattern.pattern, "job_name": pattern.job_name }) return result finally: session.close() # Run DB operation in thread pool loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _get_mapping) except Exception as e: logger.error("Failed to get project mapping", repository_name=repository_name, error=str(e)) return None async def determine_job_name(self, repository_name: str, branch_name: str) -> Optional[str]: """ Determine job name by branch Args: repository_name: repository name branch_name: branch name Returns: str: job name """ try: project = await self.get_project_mapping(repository_name) if not project: return None # 1. Check exact branch match for branch_job in project["branch_jobs"]: if branch_job["branch_name"] == branch_name: logger.debug("Found exact branch match", branch=branch_name, job=branch_job["job_name"]) return branch_job["job_name"] # 2. Check pattern match for pattern in project["branch_patterns"]: try: if re.match(pattern["pattern"], branch_name): logger.debug("Branch matched pattern", branch=branch_name, pattern=pattern["pattern"], job=pattern["job_name"]) return pattern["job_name"] except re.error as e: logger.error("Invalid regex pattern", pattern=pattern["pattern"], error=str(e)) continue # 3. Use default job if project["default_job"]: logger.debug("Using default job", branch=branch_name, job=project["default_job"]) return project["default_job"] return None except Exception as e: logger.error("Failed to determine job name", repository_name=repository_name, branch_name=branch_name, error=str(e)) return None async def log_trigger(self, log_data: Dict[str, Any]) -> bool: """ Log trigger event Args: log_data: log data Returns: bool: success or not """ try: def _log_trigger(): session = self.get_session() try: log = TriggerLog( repository_name=log_data["repository_name"], branch_name=log_data["branch_name"], commit_sha=log_data["commit_sha"], job_name=log_data["job_name"], status=log_data["status"], error_message=log_data.get("error_message") ) session.add(log) session.commit() return True except Exception as e: session.rollback() logger.error("Failed to log trigger", error=str(e)) return False finally: session.close() loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _log_trigger) except Exception as e: logger.error("Failed to log trigger", error=str(e)) return False async def get_trigger_logs(self, repository_name: str = None, branch_name: str = None, limit: int = 100) -> List[Dict[str, Any]]: """ Get trigger logs Args: repository_name: repository name (optional) branch_name: branch name (optional) limit: limit number Returns: List: log list """ try: def _get_logs(): session = self.get_session() try: query = session.query(TriggerLog) if repository_name: query = query.filter(TriggerLog.repository_name == repository_name) if branch_name: query = query.filter(TriggerLog.branch_name == branch_name) logs = query.order_by(TriggerLog.created_at.desc()).limit(limit).all() return [ { "id": log.id, "repository_name": log.repository_name, "branch_name": log.branch_name, "commit_sha": log.commit_sha, "job_name": log.job_name, "status": log.status, "error_message": log.error_message, "created_at": log.created_at.isoformat() } for log in logs ] finally: session.close() loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _get_logs) except Exception as e: logger.error("Failed to get trigger logs", error=str(e)) return [] async def create_project_mapping(self, mapping_data: Dict[str, Any]) -> bool: """ Create project mapping Args: mapping_data: mapping data Returns: bool: success or not """ try: def _create_mapping(): session = self.get_session() try: # Create project mapping project = ProjectMapping( repository_name=mapping_data["repository_name"], default_job=mapping_data.get("default_job") ) session.add(project) session.flush() # Get ID # Add branch job mappings for branch_job in mapping_data.get("branch_jobs", []): job = BranchJob( project_id=project.id, branch_name=branch_job["branch_name"], job_name=branch_job["job_name"] ) session.add(job) # Add branch pattern mappings for pattern in mapping_data.get("branch_patterns", []): pattern_obj = BranchPattern( project_id=project.id, pattern=pattern["pattern"], job_name=pattern["job_name"] ) session.add(pattern_obj) session.commit() return True except Exception as e: session.rollback() logger.error("Failed to create project mapping", error=str(e)) return False finally: session.close() loop = asyncio.get_event_loop() return await loop.run_in_executor(None, _create_mapping) except Exception as e: logger.error("Failed to create project mapping", error=str(e)) return False # Global database service instance _database_service: Optional[DatabaseService] = None def get_database_service() -> DatabaseService: """Get database service instance""" global _database_service if _database_service is None: _database_service = DatabaseService() return _database_service