refactor: organize API routers and services into subpackages

Service organization (19 files moved into 6 subpackages):
- services/instance/ — event_bus, health_monitor, lifecycle_hooks
- services/config/ — config_profile_resolver
- services/git/ — clone, git_operations, git_service
- services/build/ — docker_build, manifest_compiler
- services/terminal/ — terminal_manager, terminal_session
- services/shared/ — correlation, file_service, notification_service,
  permission_fixer, readiness_probe, ssh_keys, tunnel, workspace_manager

API router organization (16 files moved into 6 subpackages):
- api/tool/ — tool_instances, tool_types, tool_definitions,
  tool_types_validation, sessions (extracted from tool_instances)
- api/config/ — config_profiles, user_config
- api/workspace/ — workspaces, workspace_files, workspace_git,
  workspace_instances
- api/user/ — users, auth, ssh_keys
- api/project/ — projects, git_repositories
- api/system/ — health, events, notifications, dashboard, terminal,
  instance_proxy

Updated main.py imports and all __init__.py re-exports.
Sessions router extracted from tool_instances.py into api/tool/sessions.py.

Quality gates: py_compile passed, ruff passed.
This commit is contained in:
2026-06-04 12:24:14 +02:00
parent 8816ee02ce
commit 37ccaa4fdc
57 changed files with 315 additions and 163 deletions
+1 -6
View File
@@ -1,6 +1 @@
from src.api.auth import router as auth_router
from src.api.events import router as events_router
from src.api.notifications import router as notifications_router
from src.api.users import router as users_router
__all__ = ["auth_router", "events_router", "notifications_router", "users_router"]
"""API routers package."""
+6 -1
View File
@@ -1 +1,6 @@
"""Config module."""
"""Config API routers module."""
from src.api.config.config_profiles import router as config_profiles_router
from src.api.config.user_config import router as user_config_router
__all__ = ["config_profiles_router", "user_config_router"]
@@ -25,7 +25,7 @@ from src.schemas.config import (
ValidateGitUrlRequest,
ValidateGitUrlResponse,
)
from src.services.config_profile_resolver import (
from src.services.config.config_profile_resolver import (
ConfigProfileCycleError,
check_include_cycle,
resolve_profile,
@@ -726,7 +726,7 @@ async def validate_git_url(
key_path = None
if data.ssh_key_id:
from src.models import SSHKey
from src.services.ssh_keys import _get_fernet
from src.services.shared.ssh_keys import _get_fernet
try:
ssh_key_uuid = uuid.UUID(data.ssh_key_id)
+6 -1
View File
@@ -1 +1,6 @@
"""Project module."""
"""Project API routers module."""
from src.api.project.git_repositories import router as git_repositories_router
from src.api.project.projects import router as projects_router
__all__ = ["git_repositories_router", "projects_router"]
@@ -44,7 +44,7 @@ from src.utils.git_control import (
)
from src.utils.git_history import get_commit_detail, get_commit_history
from src.utils.git_url_parser import parse_git_url
from src.services.ssh_keys import _get_fernet
from src.services.shared.ssh_keys import _get_fernet
router = APIRouter(prefix="/projects", tags=["git-repositories"])
+17 -1
View File
@@ -1 +1,17 @@
"""System module."""
"""System API routers module."""
from src.api.system.dashboard import router as dashboard_router
from src.api.system.events import router as events_router
from src.api.system.health import router as health_router
from src.api.system.instance_proxy import router as instance_proxy_router
from src.api.system.notifications import router as notifications_router
from src.api.system.terminal import router as terminal_router
__all__ = [
"dashboard_router",
"events_router",
"health_router",
"instance_proxy_router",
"notifications_router",
"terminal_router",
]
@@ -10,7 +10,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import StreamingResponse
from src.auth.dependencies import get_current_user_id
from src.services.event_bus import InstanceEventBus, InstanceEventPayload
from src.services.instance.event_bus import InstanceEventBus, InstanceEventPayload
router = APIRouter(prefix="/events", tags=["events"])
@@ -10,7 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user, get_db_session
from src.models.user import User
from src.models import UserConfig
from src.services.notification_service import notification_service
from src.services.shared.notification_service import notification_service
router = APIRouter(prefix="/notifications", tags=["notifications"])
@@ -15,7 +15,7 @@ from src.auth.dependencies import get_current_user_id, get_db_session
from src.models import TerminalSessionModel
from src.models import ToolInstance
from src.models import ToolType
from src.services.terminal_manager import MaxSessionsExceededError, terminal_manager
from src.services.terminal.terminal_manager import MaxSessionsExceededError, terminal_manager
router = APIRouter()
logger = logging.getLogger(__name__)
+13 -1
View File
@@ -1 +1,13 @@
"""Tool module."""
"""Tool API routers module."""
from src.api.tool.sessions import sessions_router
from src.api.tool.tool_definitions import router as tool_definitions_router
from src.api.tool.tool_instances import router as tool_instances_router
from src.api.tool.tool_types import router as tool_types_router
__all__ = [
"sessions_router",
"tool_definitions_router",
"tool_instances_router",
"tool_types_router",
]
+80
View File
@@ -0,0 +1,80 @@
"""Sessions API endpoints (running instances for current user)."""
import uuid
from fastapi import APIRouter, Depends
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import _get_user, get_current_user_id, get_db_session
from src.models import GitRepository
from src.models import Project
from src.models import ToolInstance
from src.models import ToolType
sessions_router = APIRouter(prefix="/users", tags=["sessions"])
@sessions_router.get(
"/me/sessions",
summary="Get user sessions",
description="Get all active sessions (running instances) for the current user.",
)
async def get_user_sessions(
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
"""Get all active sessions for the current user.
Args:
user_id: ID of the authenticated user.
session: Database session.
Returns:
Dictionary containing list of active sessions with instance details.
"""
_user = await _get_user(session, user_id)
result = await session.execute(
select(ToolInstance)
.where(ToolInstance.owner_id == user_id)
.where(
ToolInstance.status.in_(
["running", "building", "pending", "stopped", "error"]
)
)
.order_by(ToolInstance.created_at.desc())
)
instances = result.scalars().all()
sessions = []
for instance in instances:
tool_type = await session.get(ToolType, instance.tool_type_id)
repo = await session.get(GitRepository, instance.repository_id)
project = await session.get(Project, instance.project_id)
sessions.append(
{
"id": str(instance.id),
"display_name": instance.display_name,
"tool_type_name": tool_type.name if tool_type else "unknown",
"tool_icon": tool_type.name if tool_type else "code",
"tool_type_interfaces": [tool_type.interface_type] if tool_type else [],
"repository_name": repo.name if repo else "unknown",
"repository_id": str(instance.repository_id),
"project_name": project.name if project else "unknown",
"project_id": str(instance.project_id),
"status": instance.status,
"url": instance.url,
"clone_mode": instance.clone_mode,
"branch": instance.branch,
"selected_config_profile_id": str(instance.selected_config_profile_id)
if instance.selected_config_profile_id
else None,
"created_at": instance.created_at.isoformat()
if instance.created_at
else None,
}
)
return {"sessions": sessions}
@@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user_id, get_db_session
from src.models import ToolDefinitionManifest
from src.models import ToolType
from src.services.manifest_compiler import (
from src.services.build.manifest_compiler import (
compile_compose,
compile_dockerfile,
compile_entrypoint,
@@ -11,7 +11,6 @@ from datetime import datetime
import httpx
from fastapi import (
APIRouter,
APIRouter as FastAPIRouter,
Depends,
HTTPException,
Request,
@@ -27,16 +26,16 @@ from src.auth.dependencies import (
get_current_user_id,
get_db_session,
)
from src.services.event_bus import InstanceEventBus
from src.services.lifecycle_hooks import publish_lifecycle_event
from src.services.instance.event_bus import InstanceEventBus
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
from src.models import ConfigProfile
from src.models import GitRepository
from src.models.project import Project
from src.models import SSHKey
from src.models import ToolInstance
from src.models import ToolType
from src.services.clone import check_dirty_state, clone_repository
from src.services.config_profile_resolver import (
from src.services.git.clone import check_dirty_state, clone_repository
from src.services.config.config_profile_resolver import (
ConfigProfileCycleError,
ResolvedProfile,
apply_resolved_profile,
@@ -61,14 +60,14 @@ from src.services.docker import (
write_config_files,
write_env_file,
)
from src.services.tunnel import (
from src.services.shared.tunnel import (
check_tunnel_health,
recreate_tunnel,
start_tunnel,
stop_tunnel,
)
from src.services.docker_build import build_image
from src.services.manifest_compiler import (
from src.services.build.docker_build import build_image
from src.services.build.manifest_compiler import (
compile_compose,
compile_dockerfile,
compile_entrypoint,
@@ -78,9 +77,9 @@ from src.services.manifest_compiler import (
merge_with_config,
resolve_base,
)
from src.services.permission_fixer import apply_mount_permissions, apply_ssh_permissions
from src.services.readiness_probe import execute_probe
from src.services.ssh_keys import cleanup_ssh_key_files, prepare_ssh_key_files
from src.services.shared.permission_fixer import apply_mount_permissions, apply_ssh_permissions
from src.services.shared.readiness_probe import execute_probe
from src.services.shared.ssh_keys import cleanup_ssh_key_files, prepare_ssh_key_files
from src.schemas.tool import CreateInstanceRequest, StartInstanceRequest
logger = logging.getLogger(__name__)
@@ -1585,7 +1584,7 @@ async def start_instance(
# Mount selected SSH keys into container home dir
if instance.ssh_key_ids:
from src.services.ssh_keys import write_ssh_config, _sanitize_filename
from src.services.shared.ssh_keys import write_ssh_config, _sanitize_filename
# Collect all valid keys first
ssh_keys_to_mount = []
@@ -2885,71 +2884,3 @@ async def proxy_to_instance(
status_code=response.status_code,
headers=response_headers,
)
sessions_router = FastAPIRouter(prefix="/users", tags=["sessions"])
@sessions_router.get(
"/me/sessions",
summary="Get user sessions",
description="Get all active sessions (running instances) for the current user.",
)
async def get_user_sessions(
user_id: uuid.UUID = Depends(get_current_user_id),
session: AsyncSession = Depends(get_db_session),
) -> dict:
"""Get all active sessions for the current user.
Args:
user_id: ID of the authenticated user.
session: Database session.
Returns:
Dictionary containing list of active sessions with instance details.
"""
_user = await _get_user(session, user_id)
result = await session.execute(
select(ToolInstance)
.where(ToolInstance.owner_id == user_id)
.where(
ToolInstance.status.in_(
["running", "building", "pending", "stopped", "error"]
)
)
.order_by(ToolInstance.created_at.desc())
)
instances = result.scalars().all()
sessions = []
for instance in instances:
tool_type = await session.get(ToolType, instance.tool_type_id)
repo = await session.get(GitRepository, instance.repository_id)
project = await session.get(Project, instance.project_id)
sessions.append(
{
"id": str(instance.id),
"display_name": instance.display_name,
"tool_type_name": tool_type.name if tool_type else "unknown",
"tool_icon": tool_type.name if tool_type else "code",
"tool_type_interfaces": [tool_type.interface_type] if tool_type else [],
"repository_name": repo.name if repo else "unknown",
"repository_id": str(instance.repository_id),
"project_name": project.name if project else "unknown",
"project_id": str(instance.project_id),
"status": instance.status,
"url": instance.url,
"clone_mode": instance.clone_mode,
"branch": instance.branch,
"selected_config_profile_id": str(instance.selected_config_profile_id)
if instance.selected_config_profile_id
else None,
"created_at": instance.created_at.isoformat()
if instance.created_at
else None,
}
)
return {"sessions": sessions}
@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src.api.tool_types_validation import (
from src.api.tool.tool_types_validation import (
check_port_exposed,
validate_compose_yaml,
validate_required_variables,
+7 -1
View File
@@ -1 +1,7 @@
"""User module."""
"""User API routers module."""
from src.api.user.auth import router as auth_router
from src.api.user.ssh_keys import router as ssh_keys_router
from src.api.user.users import router as users_router
__all__ = ["auth_router", "ssh_keys_router", "users_router"]
+14 -1
View File
@@ -1 +1,14 @@
"""Workspace module."""
"""Workspace API routers module."""
from src.api.workspace.workspace_files import router as workspace_files_router
from src.api.workspace.workspace_git import router as workspace_git_router
from src.api.workspace.workspace_instances import router as workspace_instances_router
from src.api.workspace.workspaces import all_workspaces_router, router as workspaces_router
__all__ = [
"all_workspaces_router",
"workspace_files_router",
"workspace_git_router",
"workspace_instances_router",
"workspaces_router",
]
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user_id, get_db_session
from src.models import Workspace
from src.services.file_service import FileService
from src.services.shared.file_service import FileService
router = APIRouter(prefix="/workspaces/{workspace_id}/files")
@@ -103,7 +103,7 @@ async def write_file(
raise HTTPException(status_code=400, detail=str(exc)) from exc
if commit_message:
from src.services.git_operations import GitOperations
from src.services.git.git_operations import GitOperations
git = GitOperations(workspace)
try:
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.auth.dependencies import get_current_user_id, get_db_session
from src.models import Workspace
from src.services.git_operations import GitOperations
from src.services.git.git_operations import GitOperations
router = APIRouter(prefix="/workspaces/{workspace_id}/git")
@@ -12,7 +12,7 @@ from src.auth.dependencies import get_current_user_id, get_db_session
from src.models import GitRepository
from src.models import ToolInstance
from src.models import Workspace
from src.services.workspace_manager import WorkspaceHasInstancesError, WorkspaceManager
from src.services.shared.workspace_manager import WorkspaceHasInstancesError, WorkspaceManager
logger = logging.getLogger(__name__)
+1 -1
View File
@@ -10,7 +10,7 @@ from collections.abc import Callable
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from src.services.correlation import get_correlation_id
from src.services.shared.correlation import get_correlation_id
logger = logging.getLogger(__name__)
+24 -21
View File
@@ -7,27 +7,30 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from src.api.auth import router as auth_router
from src.api.dashboard import router as dashboard_router
from src.api.events import router as events_router
from src.api.git_repositories import router as git_repositories_router
from src.api.health import router as health_router
from src.api.projects import router as projects_router
from src.api.ssh_keys import router as ssh_keys_router
from src.api.terminal import router as terminal_router
from src.api.instance_proxy import router as instance_proxy_router
from src.api.config_profiles import router as config_profiles_router
from src.api.tool_definitions import router as tool_definitions_router
from src.api.tool_instances import router as tool_instances_router
from src.api.tool_instances import sessions_router
from src.api.tool_types import router as tool_types_router
from src.api.notifications import router as notifications_router
from src.api.user_config import router as user_config_router
from src.api.users import router as users_router
from src.api.workspace_files import router as workspace_files_router
from src.api.workspace_git import router as workspace_git_router
from src.api.workspace_instances import router as workspace_instances_router
from src.api.workspaces import all_workspaces_router, router as workspaces_router
from src.api.config import config_profiles_router, user_config_router
from src.api.project import git_repositories_router, projects_router
from src.api.system import (
dashboard_router,
events_router,
health_router,
instance_proxy_router,
notifications_router,
terminal_router,
)
from src.api.tool import (
sessions_router,
tool_definitions_router,
tool_instances_router,
tool_types_router,
)
from src.api.user import auth_router, ssh_keys_router, users_router
from src.api.workspace import (
all_workspaces_router,
workspace_files_router,
workspace_git_router,
workspace_instances_router,
workspaces_router,
)
from src.config import Settings
from src.models import Notification # noqa: F401 Alembic model discovery
from src.models import TerminalSessionModel # noqa: F401 Alembic model discovery
+1 -1
View File
@@ -6,7 +6,7 @@ from datetime import datetime
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
from src.api.tool_types_validation import (
from src.api.tool.tool_types_validation import (
check_port_exposed,
validate_compose_yaml,
validate_required_variables,
+25 -1
View File
@@ -1 +1,25 @@
"""Config module."""
"""Config profile services module."""
from src.services.config.config_profile_resolver import (
ConfigProfileCycleError,
ConfigProfileNotFoundError,
ResolvedMount,
ResolvedProfile,
apply_resolved_profile,
check_include_cycle,
expand_container_path,
resolve_profile,
resolved_profile_to_dict,
)
__all__ = [
"ConfigProfileCycleError",
"ConfigProfileNotFoundError",
"ResolvedMount",
"ResolvedProfile",
"apply_resolved_profile",
"check_include_cycle",
"expand_container_path",
"resolve_profile",
"resolved_profile_to_dict",
]
+19 -1
View File
@@ -1 +1,19 @@
"""Git module."""
"""Git services module."""
from src.services.git.clone import (
check_dirty_state,
clone_repository,
remove_clone_directory,
)
from src.services.git.git_operations import Commit, GitOperations, GitStatus
from src.services.git.git_service import GitService
__all__ = [
"check_dirty_state",
"clone_repository",
"remove_clone_directory",
"Commit",
"GitOperations",
"GitStatus",
"GitService",
]
+7 -1
View File
@@ -1 +1,7 @@
"""Instance module."""
"""Instance lifecycle services module."""
from src.services.instance.event_bus import InstanceEventBus
from src.services.instance.health_monitor import HealthMonitor
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
__all__ = ["InstanceEventBus", "HealthMonitor", "publish_lifecycle_event"]
@@ -12,11 +12,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.database import SessionLocal
from src.models import HealthCheck
from src.models import ToolInstance
from src.services.correlation import get_correlation_id
from src.services.shared.correlation import get_correlation_id
from src.services.docker import get_container_status
from src.services.tunnel import check_tunnel_health
from src.services.event_bus import InstanceEventBus, InstanceEventPayload
from src.services.notification_service import notification_service
from src.services.shared.tunnel import check_tunnel_health
from src.services.instance.event_bus import InstanceEventBus, InstanceEventPayload
from src.services.shared.notification_service import notification_service
logger = logging.getLogger(__name__)
@@ -8,9 +8,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src.models import InstanceEvent
from src.models import ToolInstance
from src.services.correlation import get_correlation_id
from src.services.event_bus import InstanceEventBus, InstanceEventPayload
from src.services.notification_service import notification_service
from src.services.shared.correlation import get_correlation_id
from src.services.instance.event_bus import InstanceEventBus, InstanceEventPayload
from src.services.shared.notification_service import notification_service
logger = logging.getLogger(__name__)
+51 -1
View File
@@ -1 +1,51 @@
"""Shared module."""
"""Shared services module."""
from src.services.shared.correlation import CorrelationIdMiddleware, get_correlation_id
from src.services.shared.file_service import FileEntry, FileService
from src.services.shared.notification_service import NotificationService
from src.services.shared.permission_fixer import (
PermissionFixError,
apply_mount_permissions,
apply_ssh_permissions,
check_root_user_available,
)
from src.services.shared.readiness_probe import execute_probe
from src.services.shared.ssh_keys import (
cleanup_ssh_key_files,
prepare_ssh_key_files,
write_ssh_config,
)
from src.services.shared.tunnel import (
check_tunnel_health,
recreate_tunnel,
start_tunnel,
stop_tunnel,
)
from src.services.shared.workspace_manager import (
SyncResult,
WorkspaceHasInstancesError,
WorkspaceManager,
)
__all__ = [
"CorrelationIdMiddleware",
"FileEntry",
"FileService",
"NotificationService",
"PermissionFixError",
"SyncResult",
"WorkspaceHasInstancesError",
"WorkspaceManager",
"apply_mount_permissions",
"apply_ssh_permissions",
"check_root_user_available",
"check_tunnel_health",
"cleanup_ssh_key_files",
"execute_probe",
"get_correlation_id",
"prepare_ssh_key_files",
"recreate_tunnel",
"start_tunnel",
"stop_tunnel",
"write_ssh_config",
]
@@ -15,8 +15,8 @@ from typing import TYPE_CHECKING
from sqlalchemy import select
from src.models import Workspace
from src.services.git_service import GitService
from src.services.ssh_keys import _get_fernet
from src.services.git.git_service import GitService
from src.services.shared.ssh_keys import _get_fernet
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
+9 -1
View File
@@ -1 +1,9 @@
"""Terminal module."""
"""Terminal services module."""
from src.services.terminal.terminal_manager import (
MaxSessionsExceededError,
TerminalManager,
)
from src.services.terminal.terminal_session import TerminalSession
__all__ = ["MaxSessionsExceededError", "TerminalManager", "TerminalSession"]
@@ -10,7 +10,7 @@ from sqlalchemy.dialects.postgresql import insert as pg_insert
from src.database import SessionLocal
from src.models import TerminalSessionModel
from src.services.terminal_session import TerminalSession
from src.services.terminal.terminal_session import TerminalSession
logger = logging.getLogger(__name__)