Add SQLite database integration with SQLAlchemy async, migration tooling, and comprehensive CRUD operations for hosts, tasks, schedules, and logs
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
from .host import HostRepository
|
||||
from .bootstrap_status import BootstrapStatusRepository
|
||||
from .task import TaskRepository
|
||||
from .schedule import ScheduleRepository
|
||||
from .log import LogRepository
|
||||
|
||||
__all__ = [
|
||||
"HostRepository",
|
||||
"BootstrapStatusRepository",
|
||||
"TaskRepository",
|
||||
"ScheduleRepository",
|
||||
"LogRepository",
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.bootstrap_status import BootstrapStatus
|
||||
|
||||
|
||||
class BootstrapStatusRepository:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def list_for_host(self, host_id: str) -> list[BootstrapStatus]:
|
||||
stmt = select(BootstrapStatus).where(BootstrapStatus.host_id == host_id).order_by(BootstrapStatus.created_at.desc())
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def latest_for_host(self, host_id: str) -> Optional[BootstrapStatus]:
|
||||
stmt = (
|
||||
select(BootstrapStatus)
|
||||
.where(BootstrapStatus.host_id == host_id)
|
||||
.order_by(BootstrapStatus.created_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def create(self, *, host_id: str, status: str, automation_user: Optional[str] = None,
|
||||
last_attempt=None, error_message: Optional[str] = None) -> BootstrapStatus:
|
||||
record = BootstrapStatus(
|
||||
host_id=host_id,
|
||||
status=status,
|
||||
automation_user=automation_user,
|
||||
last_attempt=last_attempt,
|
||||
error_message=error_message,
|
||||
)
|
||||
self.session.add(record)
|
||||
await self.session.flush()
|
||||
return record
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Iterable, Optional
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from models.host import Host
|
||||
|
||||
|
||||
class HostRepository:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def list(self, limit: int = 100, offset: int = 0, include_deleted: bool = False) -> list[Host]:
|
||||
stmt = select(Host).order_by(Host.created_at.desc()).offset(offset).limit(limit)
|
||||
if not include_deleted:
|
||||
stmt = stmt.where(Host.deleted_at.is_(None))
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get(self, host_id: str, include_deleted: bool = False) -> Optional[Host]:
|
||||
stmt = select(Host).where(Host.id == host_id).options(selectinload(Host.bootstrap_statuses))
|
||||
if not include_deleted:
|
||||
stmt = stmt.where(Host.deleted_at.is_(None))
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_by_ip(self, ip_address: str, include_deleted: bool = False) -> Optional[Host]:
|
||||
stmt = select(Host).where(Host.ip_address == ip_address)
|
||||
if not include_deleted:
|
||||
stmt = stmt.where(Host.deleted_at.is_(None))
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def create(self, *, id: str, name: str, ip_address: str, ansible_group: Optional[str] = None,
|
||||
status: str = "unknown", reachable: bool = False, last_seen: Optional[datetime] = None) -> Host:
|
||||
host = Host(
|
||||
id=id,
|
||||
name=name,
|
||||
ip_address=ip_address,
|
||||
ansible_group=ansible_group,
|
||||
status=status,
|
||||
reachable=reachable,
|
||||
last_seen=last_seen,
|
||||
)
|
||||
self.session.add(host)
|
||||
await self.session.flush()
|
||||
return host
|
||||
|
||||
async def update(self, host: Host, **fields) -> Host:
|
||||
for key, value in fields.items():
|
||||
if value is not None:
|
||||
setattr(host, key, value)
|
||||
await self.session.flush()
|
||||
return host
|
||||
|
||||
async def soft_delete(self, host_id: str) -> bool:
|
||||
stmt = (
|
||||
update(Host)
|
||||
.where(Host.id == host_id, Host.deleted_at.is_(None))
|
||||
.values(deleted_at=datetime.now(timezone.utc))
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.rowcount > 0
|
||||
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.log import Log
|
||||
|
||||
|
||||
class LogRepository:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def list(self, limit: int = 100, offset: int = 0, level: Optional[str] = None, source: Optional[str] = None) -> list[Log]:
|
||||
stmt = select(Log).order_by(Log.created_at.desc()).offset(offset).limit(limit)
|
||||
if level:
|
||||
stmt = stmt.where(Log.level == level)
|
||||
if source:
|
||||
stmt = stmt.where(Log.source == source)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def create(self, **fields) -> Log:
|
||||
log = Log(**fields)
|
||||
self.session.add(log)
|
||||
await self.session.flush()
|
||||
return log
|
||||
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from models.schedule import Schedule
|
||||
from models.schedule_run import ScheduleRun
|
||||
|
||||
|
||||
class ScheduleRepository:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def list(self, limit: int = 100, offset: int = 0, include_deleted: bool = False) -> list[Schedule]:
|
||||
stmt = select(Schedule).order_by(Schedule.created_at.desc()).offset(offset).limit(limit)
|
||||
if not include_deleted:
|
||||
stmt = stmt.where(Schedule.deleted_at.is_(None))
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get(self, schedule_id: str, include_deleted: bool = False) -> Optional[Schedule]:
|
||||
stmt = select(Schedule).where(Schedule.id == schedule_id).options(
|
||||
selectinload(Schedule.runs)
|
||||
)
|
||||
if not include_deleted:
|
||||
stmt = stmt.where(Schedule.deleted_at.is_(None))
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def create(self, **fields) -> Schedule:
|
||||
schedule = Schedule(**fields)
|
||||
self.session.add(schedule)
|
||||
await self.session.flush()
|
||||
return schedule
|
||||
|
||||
async def update(self, schedule: Schedule, **fields) -> Schedule:
|
||||
for key, value in fields.items():
|
||||
if value is not None:
|
||||
setattr(schedule, key, value)
|
||||
await self.session.flush()
|
||||
return schedule
|
||||
|
||||
async def soft_delete(self, schedule_id: str) -> bool:
|
||||
stmt = (
|
||||
update(Schedule)
|
||||
.where(Schedule.id == schedule_id, Schedule.deleted_at.is_(None))
|
||||
.values(deleted_at=datetime.now(timezone.utc))
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.rowcount > 0
|
||||
|
||||
async def add_run(self, **fields) -> ScheduleRun:
|
||||
run = ScheduleRun(**fields)
|
||||
self.session.add(run)
|
||||
await self.session.flush()
|
||||
return run
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.schedule_run import ScheduleRun
|
||||
|
||||
|
||||
class ScheduleRunRepository:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def get(self, run_id: int) -> Optional[ScheduleRun]:
|
||||
stmt = select(ScheduleRun).where(ScheduleRun.id == run_id)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def list_for_schedule(self, schedule_id: str, limit: int = 100, offset: int = 0) -> list[ScheduleRun]:
|
||||
stmt = (
|
||||
select(ScheduleRun)
|
||||
.where(ScheduleRun.schedule_id == schedule_id)
|
||||
.order_by(ScheduleRun.started_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def create(self, **fields) -> ScheduleRun:
|
||||
run = ScheduleRun(**fields)
|
||||
self.session.add(run)
|
||||
await self.session.flush()
|
||||
return run
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from models.task import Task
|
||||
|
||||
|
||||
class TaskRepository:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def list(self, limit: int = 100, offset: int = 0) -> list[Task]:
|
||||
stmt = select(Task).order_by(Task.created_at.desc()).offset(offset).limit(limit)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
async def get(self, task_id: str) -> Optional[Task]:
|
||||
stmt = select(Task).where(Task.id == task_id).options(selectinload(Task.schedule_runs))
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def create(self, *, id: str, action: str, target: str, playbook: Optional[str] = None,
|
||||
status: str = "pending") -> Task:
|
||||
task = Task(
|
||||
id=id,
|
||||
action=action,
|
||||
target=target,
|
||||
playbook=playbook,
|
||||
status=status,
|
||||
)
|
||||
self.session.add(task)
|
||||
await self.session.flush()
|
||||
return task
|
||||
|
||||
async def update(self, task: Task, **fields) -> Task:
|
||||
for key, value in fields.items():
|
||||
if value is not None:
|
||||
setattr(task, key, value)
|
||||
await self.session.flush()
|
||||
return task
|
||||
Reference in New Issue
Block a user