From 37ccaa4fdc839eaf1849c9a8d3aa357221fecad6 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Thu, 4 Jun 2026 12:24:14 +0200 Subject: [PATCH] refactor: organize API routers and services into subpackages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- apps/api/src/api/__init__.py | 7 +- apps/api/src/api/config/__init__.py | 7 +- .../src/api/{ => config}/config_profiles.py | 4 +- apps/api/src/api/{ => config}/user_config.py | 0 apps/api/src/api/project/__init__.py | 7 +- .../src/api/{ => project}/git_repositories.py | 2 +- apps/api/src/api/{ => project}/projects.py | 0 apps/api/src/api/system/__init__.py | 18 +++- apps/api/src/api/{ => system}/dashboard.py | 0 apps/api/src/api/{ => system}/events.py | 2 +- apps/api/src/api/{ => system}/health.py | 0 .../src/api/{ => system}/instance_proxy.py | 0 .../api/src/api/{ => system}/notifications.py | 2 +- apps/api/src/api/{ => system}/terminal.py | 2 +- apps/api/src/api/tool/__init__.py | 14 ++- apps/api/src/api/tool/sessions.py | 80 ++++++++++++++++ .../src/api/{ => tool}/tool_definitions.py | 2 +- apps/api/src/api/{ => tool}/tool_instances.py | 91 +++---------------- apps/api/src/api/{ => tool}/tool_types.py | 2 +- .../api/{ => tool}/tool_types_validation.py | 0 apps/api/src/api/user/__init__.py | 8 +- apps/api/src/api/{ => user}/auth.py | 0 apps/api/src/api/{ => user}/ssh_keys.py | 0 apps/api/src/api/{ => user}/users.py | 0 apps/api/src/api/workspace/__init__.py | 15 ++- .../api/{ => workspace}/workspace_files.py | 4 +- .../src/api/{ => workspace}/workspace_git.py | 2 +- .../{ => workspace}/workspace_instances.py | 0 .../api/src/api/{ => workspace}/workspaces.py | 2 +- apps/api/src/logging_config.py | 2 +- apps/api/src/main.py | 45 ++++----- apps/api/src/schemas/tool/tool_type.py | 2 +- .../src/services/{ => build}/docker_build.py | 0 .../services/{ => build}/manifest_compiler.py | 0 apps/api/src/services/config/__init__.py | 26 +++++- .../{ => config}/config_profile_resolver.py | 0 apps/api/src/services/git/__init__.py | 20 +++- apps/api/src/services/{ => git}/clone.py | 0 .../src/services/{ => git}/git_operations.py | 0 .../api/src/services/{ => git}/git_service.py | 0 apps/api/src/services/instance/__init__.py | 8 +- .../src/services/{ => instance}/event_bus.py | 0 .../services/{ => instance}/health_monitor.py | 8 +- .../{ => instance}/lifecycle_hooks.py | 6 +- apps/api/src/services/shared/__init__.py | 52 ++++++++++- .../src/services/{ => shared}/correlation.py | 0 .../src/services/{ => shared}/file_service.py | 0 .../{ => shared}/notification_service.py | 0 .../services/{ => shared}/permission_fixer.py | 0 .../services/{ => shared}/readiness_probe.py | 0 .../api/src/services/{ => shared}/ssh_keys.py | 0 apps/api/src/services/{ => shared}/tunnel.py | 0 .../{ => shared}/workspace_manager.py | 4 +- apps/api/src/services/terminal/__init__.py | 10 +- .../{ => terminal}/terminal_manager.py | 2 +- .../{ => terminal}/terminal_session.py | 0 progress.md | 22 +---- 57 files changed, 315 insertions(+), 163 deletions(-) rename apps/api/src/api/{ => config}/config_profiles.py (99%) rename apps/api/src/api/{ => config}/user_config.py (100%) rename apps/api/src/api/{ => project}/git_repositories.py (99%) rename apps/api/src/api/{ => project}/projects.py (100%) rename apps/api/src/api/{ => system}/dashboard.py (100%) rename apps/api/src/api/{ => system}/events.py (96%) rename apps/api/src/api/{ => system}/health.py (100%) rename apps/api/src/api/{ => system}/instance_proxy.py (100%) rename apps/api/src/api/{ => system}/notifications.py (98%) rename apps/api/src/api/{ => system}/terminal.py (99%) create mode 100644 apps/api/src/api/tool/sessions.py rename apps/api/src/api/{ => tool}/tool_definitions.py (99%) rename apps/api/src/api/{ => tool}/tool_instances.py (97%) rename apps/api/src/api/{ => tool}/tool_types.py (99%) rename apps/api/src/api/{ => tool}/tool_types_validation.py (100%) rename apps/api/src/api/{ => user}/auth.py (100%) rename apps/api/src/api/{ => user}/ssh_keys.py (100%) rename apps/api/src/api/{ => user}/users.py (100%) rename apps/api/src/api/{ => workspace}/workspace_files.py (96%) rename apps/api/src/api/{ => workspace}/workspace_git.py (99%) rename apps/api/src/api/{ => workspace}/workspace_instances.py (100%) rename apps/api/src/api/{ => workspace}/workspaces.py (99%) rename apps/api/src/services/{ => build}/docker_build.py (100%) rename apps/api/src/services/{ => build}/manifest_compiler.py (100%) rename apps/api/src/services/{ => config}/config_profile_resolver.py (100%) rename apps/api/src/services/{ => git}/clone.py (100%) rename apps/api/src/services/{ => git}/git_operations.py (100%) rename apps/api/src/services/{ => git}/git_service.py (100%) rename apps/api/src/services/{ => instance}/event_bus.py (100%) rename apps/api/src/services/{ => instance}/health_monitor.py (96%) rename apps/api/src/services/{ => instance}/lifecycle_hooks.py (95%) rename apps/api/src/services/{ => shared}/correlation.py (100%) rename apps/api/src/services/{ => shared}/file_service.py (100%) rename apps/api/src/services/{ => shared}/notification_service.py (100%) rename apps/api/src/services/{ => shared}/permission_fixer.py (100%) rename apps/api/src/services/{ => shared}/readiness_probe.py (100%) rename apps/api/src/services/{ => shared}/ssh_keys.py (100%) rename apps/api/src/services/{ => shared}/tunnel.py (100%) rename apps/api/src/services/{ => shared}/workspace_manager.py (98%) rename apps/api/src/services/{ => terminal}/terminal_manager.py (99%) rename apps/api/src/services/{ => terminal}/terminal_session.py (100%) diff --git a/apps/api/src/api/__init__.py b/apps/api/src/api/__init__.py index ccb9492..010fe3a 100644 --- a/apps/api/src/api/__init__.py +++ b/apps/api/src/api/__init__.py @@ -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.""" diff --git a/apps/api/src/api/config/__init__.py b/apps/api/src/api/config/__init__.py index da7ff7c..57afda4 100644 --- a/apps/api/src/api/config/__init__.py +++ b/apps/api/src/api/config/__init__.py @@ -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"] diff --git a/apps/api/src/api/config_profiles.py b/apps/api/src/api/config/config_profiles.py similarity index 99% rename from apps/api/src/api/config_profiles.py rename to apps/api/src/api/config/config_profiles.py index 2b383b6..5b0d3d6 100644 --- a/apps/api/src/api/config_profiles.py +++ b/apps/api/src/api/config/config_profiles.py @@ -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) diff --git a/apps/api/src/api/user_config.py b/apps/api/src/api/config/user_config.py similarity index 100% rename from apps/api/src/api/user_config.py rename to apps/api/src/api/config/user_config.py diff --git a/apps/api/src/api/project/__init__.py b/apps/api/src/api/project/__init__.py index 1ed2378..d5bfbaf 100644 --- a/apps/api/src/api/project/__init__.py +++ b/apps/api/src/api/project/__init__.py @@ -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"] diff --git a/apps/api/src/api/git_repositories.py b/apps/api/src/api/project/git_repositories.py similarity index 99% rename from apps/api/src/api/git_repositories.py rename to apps/api/src/api/project/git_repositories.py index 3373746..920f8d4 100644 --- a/apps/api/src/api/git_repositories.py +++ b/apps/api/src/api/project/git_repositories.py @@ -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"]) diff --git a/apps/api/src/api/projects.py b/apps/api/src/api/project/projects.py similarity index 100% rename from apps/api/src/api/projects.py rename to apps/api/src/api/project/projects.py diff --git a/apps/api/src/api/system/__init__.py b/apps/api/src/api/system/__init__.py index 3f55c57..99bbe44 100644 --- a/apps/api/src/api/system/__init__.py +++ b/apps/api/src/api/system/__init__.py @@ -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", +] diff --git a/apps/api/src/api/dashboard.py b/apps/api/src/api/system/dashboard.py similarity index 100% rename from apps/api/src/api/dashboard.py rename to apps/api/src/api/system/dashboard.py diff --git a/apps/api/src/api/events.py b/apps/api/src/api/system/events.py similarity index 96% rename from apps/api/src/api/events.py rename to apps/api/src/api/system/events.py index 3b33859..42720b8 100644 --- a/apps/api/src/api/events.py +++ b/apps/api/src/api/system/events.py @@ -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"]) diff --git a/apps/api/src/api/health.py b/apps/api/src/api/system/health.py similarity index 100% rename from apps/api/src/api/health.py rename to apps/api/src/api/system/health.py diff --git a/apps/api/src/api/instance_proxy.py b/apps/api/src/api/system/instance_proxy.py similarity index 100% rename from apps/api/src/api/instance_proxy.py rename to apps/api/src/api/system/instance_proxy.py diff --git a/apps/api/src/api/notifications.py b/apps/api/src/api/system/notifications.py similarity index 98% rename from apps/api/src/api/notifications.py rename to apps/api/src/api/system/notifications.py index 5d51848..f2f9151 100644 --- a/apps/api/src/api/notifications.py +++ b/apps/api/src/api/system/notifications.py @@ -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"]) diff --git a/apps/api/src/api/terminal.py b/apps/api/src/api/system/terminal.py similarity index 99% rename from apps/api/src/api/terminal.py rename to apps/api/src/api/system/terminal.py index ce4fcd8..0175ae4 100644 --- a/apps/api/src/api/terminal.py +++ b/apps/api/src/api/system/terminal.py @@ -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__) diff --git a/apps/api/src/api/tool/__init__.py b/apps/api/src/api/tool/__init__.py index b05b87d..f525de3 100644 --- a/apps/api/src/api/tool/__init__.py +++ b/apps/api/src/api/tool/__init__.py @@ -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", +] diff --git a/apps/api/src/api/tool/sessions.py b/apps/api/src/api/tool/sessions.py new file mode 100644 index 0000000..d25ceb5 --- /dev/null +++ b/apps/api/src/api/tool/sessions.py @@ -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} diff --git a/apps/api/src/api/tool_definitions.py b/apps/api/src/api/tool/tool_definitions.py similarity index 99% rename from apps/api/src/api/tool_definitions.py rename to apps/api/src/api/tool/tool_definitions.py index 96e28e7..0a59515 100644 --- a/apps/api/src/api/tool_definitions.py +++ b/apps/api/src/api/tool/tool_definitions.py @@ -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, diff --git a/apps/api/src/api/tool_instances.py b/apps/api/src/api/tool/tool_instances.py similarity index 97% rename from apps/api/src/api/tool_instances.py rename to apps/api/src/api/tool/tool_instances.py index 125dc4d..4921039 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool/tool_instances.py @@ -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} diff --git a/apps/api/src/api/tool_types.py b/apps/api/src/api/tool/tool_types.py similarity index 99% rename from apps/api/src/api/tool_types.py rename to apps/api/src/api/tool/tool_types.py index 49920c0..729510a 100644 --- a/apps/api/src/api/tool_types.py +++ b/apps/api/src/api/tool/tool_types.py @@ -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, diff --git a/apps/api/src/api/tool_types_validation.py b/apps/api/src/api/tool/tool_types_validation.py similarity index 100% rename from apps/api/src/api/tool_types_validation.py rename to apps/api/src/api/tool/tool_types_validation.py diff --git a/apps/api/src/api/user/__init__.py b/apps/api/src/api/user/__init__.py index 7f42956..4312eb1 100644 --- a/apps/api/src/api/user/__init__.py +++ b/apps/api/src/api/user/__init__.py @@ -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"] diff --git a/apps/api/src/api/auth.py b/apps/api/src/api/user/auth.py similarity index 100% rename from apps/api/src/api/auth.py rename to apps/api/src/api/user/auth.py diff --git a/apps/api/src/api/ssh_keys.py b/apps/api/src/api/user/ssh_keys.py similarity index 100% rename from apps/api/src/api/ssh_keys.py rename to apps/api/src/api/user/ssh_keys.py diff --git a/apps/api/src/api/users.py b/apps/api/src/api/user/users.py similarity index 100% rename from apps/api/src/api/users.py rename to apps/api/src/api/user/users.py diff --git a/apps/api/src/api/workspace/__init__.py b/apps/api/src/api/workspace/__init__.py index e2d8117..07902ef 100644 --- a/apps/api/src/api/workspace/__init__.py +++ b/apps/api/src/api/workspace/__init__.py @@ -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", +] diff --git a/apps/api/src/api/workspace_files.py b/apps/api/src/api/workspace/workspace_files.py similarity index 96% rename from apps/api/src/api/workspace_files.py rename to apps/api/src/api/workspace/workspace_files.py index f43a2f8..eecfc3a 100644 --- a/apps/api/src/api/workspace_files.py +++ b/apps/api/src/api/workspace/workspace_files.py @@ -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: diff --git a/apps/api/src/api/workspace_git.py b/apps/api/src/api/workspace/workspace_git.py similarity index 99% rename from apps/api/src/api/workspace_git.py rename to apps/api/src/api/workspace/workspace_git.py index d5c8b91..d7fad60 100644 --- a/apps/api/src/api/workspace_git.py +++ b/apps/api/src/api/workspace/workspace_git.py @@ -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") diff --git a/apps/api/src/api/workspace_instances.py b/apps/api/src/api/workspace/workspace_instances.py similarity index 100% rename from apps/api/src/api/workspace_instances.py rename to apps/api/src/api/workspace/workspace_instances.py diff --git a/apps/api/src/api/workspaces.py b/apps/api/src/api/workspace/workspaces.py similarity index 99% rename from apps/api/src/api/workspaces.py rename to apps/api/src/api/workspace/workspaces.py index 4ce8938..f860e3a 100644 --- a/apps/api/src/api/workspaces.py +++ b/apps/api/src/api/workspace/workspaces.py @@ -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__) diff --git a/apps/api/src/logging_config.py b/apps/api/src/logging_config.py index 31ff758..243938b 100644 --- a/apps/api/src/logging_config.py +++ b/apps/api/src/logging_config.py @@ -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__) diff --git a/apps/api/src/main.py b/apps/api/src/main.py index 875025b..fecd875 100644 --- a/apps/api/src/main.py +++ b/apps/api/src/main.py @@ -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 diff --git a/apps/api/src/schemas/tool/tool_type.py b/apps/api/src/schemas/tool/tool_type.py index 34672d0..f21a878 100644 --- a/apps/api/src/schemas/tool/tool_type.py +++ b/apps/api/src/schemas/tool/tool_type.py @@ -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, diff --git a/apps/api/src/services/docker_build.py b/apps/api/src/services/build/docker_build.py similarity index 100% rename from apps/api/src/services/docker_build.py rename to apps/api/src/services/build/docker_build.py diff --git a/apps/api/src/services/manifest_compiler.py b/apps/api/src/services/build/manifest_compiler.py similarity index 100% rename from apps/api/src/services/manifest_compiler.py rename to apps/api/src/services/build/manifest_compiler.py diff --git a/apps/api/src/services/config/__init__.py b/apps/api/src/services/config/__init__.py index da7ff7c..1ebf33f 100644 --- a/apps/api/src/services/config/__init__.py +++ b/apps/api/src/services/config/__init__.py @@ -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", +] diff --git a/apps/api/src/services/config_profile_resolver.py b/apps/api/src/services/config/config_profile_resolver.py similarity index 100% rename from apps/api/src/services/config_profile_resolver.py rename to apps/api/src/services/config/config_profile_resolver.py diff --git a/apps/api/src/services/git/__init__.py b/apps/api/src/services/git/__init__.py index 258e60f..bee0668 100644 --- a/apps/api/src/services/git/__init__.py +++ b/apps/api/src/services/git/__init__.py @@ -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", +] diff --git a/apps/api/src/services/clone.py b/apps/api/src/services/git/clone.py similarity index 100% rename from apps/api/src/services/clone.py rename to apps/api/src/services/git/clone.py diff --git a/apps/api/src/services/git_operations.py b/apps/api/src/services/git/git_operations.py similarity index 100% rename from apps/api/src/services/git_operations.py rename to apps/api/src/services/git/git_operations.py diff --git a/apps/api/src/services/git_service.py b/apps/api/src/services/git/git_service.py similarity index 100% rename from apps/api/src/services/git_service.py rename to apps/api/src/services/git/git_service.py diff --git a/apps/api/src/services/instance/__init__.py b/apps/api/src/services/instance/__init__.py index 0d47d65..3bbab89 100644 --- a/apps/api/src/services/instance/__init__.py +++ b/apps/api/src/services/instance/__init__.py @@ -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"] diff --git a/apps/api/src/services/event_bus.py b/apps/api/src/services/instance/event_bus.py similarity index 100% rename from apps/api/src/services/event_bus.py rename to apps/api/src/services/instance/event_bus.py diff --git a/apps/api/src/services/health_monitor.py b/apps/api/src/services/instance/health_monitor.py similarity index 96% rename from apps/api/src/services/health_monitor.py rename to apps/api/src/services/instance/health_monitor.py index bbabea4..49b86cc 100644 --- a/apps/api/src/services/health_monitor.py +++ b/apps/api/src/services/instance/health_monitor.py @@ -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__) diff --git a/apps/api/src/services/lifecycle_hooks.py b/apps/api/src/services/instance/lifecycle_hooks.py similarity index 95% rename from apps/api/src/services/lifecycle_hooks.py rename to apps/api/src/services/instance/lifecycle_hooks.py index f791366..3a69d6e 100644 --- a/apps/api/src/services/lifecycle_hooks.py +++ b/apps/api/src/services/instance/lifecycle_hooks.py @@ -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__) diff --git a/apps/api/src/services/shared/__init__.py b/apps/api/src/services/shared/__init__.py index 3313f18..9fcbb55 100644 --- a/apps/api/src/services/shared/__init__.py +++ b/apps/api/src/services/shared/__init__.py @@ -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", +] diff --git a/apps/api/src/services/correlation.py b/apps/api/src/services/shared/correlation.py similarity index 100% rename from apps/api/src/services/correlation.py rename to apps/api/src/services/shared/correlation.py diff --git a/apps/api/src/services/file_service.py b/apps/api/src/services/shared/file_service.py similarity index 100% rename from apps/api/src/services/file_service.py rename to apps/api/src/services/shared/file_service.py diff --git a/apps/api/src/services/notification_service.py b/apps/api/src/services/shared/notification_service.py similarity index 100% rename from apps/api/src/services/notification_service.py rename to apps/api/src/services/shared/notification_service.py diff --git a/apps/api/src/services/permission_fixer.py b/apps/api/src/services/shared/permission_fixer.py similarity index 100% rename from apps/api/src/services/permission_fixer.py rename to apps/api/src/services/shared/permission_fixer.py diff --git a/apps/api/src/services/readiness_probe.py b/apps/api/src/services/shared/readiness_probe.py similarity index 100% rename from apps/api/src/services/readiness_probe.py rename to apps/api/src/services/shared/readiness_probe.py diff --git a/apps/api/src/services/ssh_keys.py b/apps/api/src/services/shared/ssh_keys.py similarity index 100% rename from apps/api/src/services/ssh_keys.py rename to apps/api/src/services/shared/ssh_keys.py diff --git a/apps/api/src/services/tunnel.py b/apps/api/src/services/shared/tunnel.py similarity index 100% rename from apps/api/src/services/tunnel.py rename to apps/api/src/services/shared/tunnel.py diff --git a/apps/api/src/services/workspace_manager.py b/apps/api/src/services/shared/workspace_manager.py similarity index 98% rename from apps/api/src/services/workspace_manager.py rename to apps/api/src/services/shared/workspace_manager.py index 49ab6f8..972ddaf 100644 --- a/apps/api/src/services/workspace_manager.py +++ b/apps/api/src/services/shared/workspace_manager.py @@ -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 diff --git a/apps/api/src/services/terminal/__init__.py b/apps/api/src/services/terminal/__init__.py index f12436c..80512af 100644 --- a/apps/api/src/services/terminal/__init__.py +++ b/apps/api/src/services/terminal/__init__.py @@ -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"] diff --git a/apps/api/src/services/terminal_manager.py b/apps/api/src/services/terminal/terminal_manager.py similarity index 99% rename from apps/api/src/services/terminal_manager.py rename to apps/api/src/services/terminal/terminal_manager.py index f37d9e9..9960a78 100644 --- a/apps/api/src/services/terminal_manager.py +++ b/apps/api/src/services/terminal/terminal_manager.py @@ -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__) diff --git a/apps/api/src/services/terminal_session.py b/apps/api/src/services/terminal/terminal_session.py similarity index 100% rename from apps/api/src/services/terminal_session.py rename to apps/api/src/services/terminal/terminal_session.py diff --git a/progress.md b/progress.md index 901f838..9ad8275 100644 --- a/progress.md +++ b/progress.md @@ -1,30 +1,10 @@ # Progress ## Status -In Progress — Backend structural refactoring +In Progress ## Tasks -- [x] Phase 0: Create submodule directory structure (__init__.py files) -- [x] Phase 1: Model subpackages (tool, config, user, project, system) -- [x] Phase 2: Docker service package split (compose, container, config_staging, tunnel) -- [ ] Phase 3: Service subpackages (instance, config, git, build, terminal, shared) -- [ ] Phase 4: Schema extraction (tool, config, user, project, system) -- [ ] Phase 5: API router subpackages (tool, config, workspace, user, project, system) -- [ ] Phase 6: Auth dependency refactor -- [ ] Phase 7: Frontend reorganization -- [ ] Phase 8: Integration and verification ## Files Changed -- `apps/api/src/services/docker.py` → deleted (split into package) -- `apps/api/src/services/docker/__init__.py` — re-exports all public functions -- `apps/api/src/services/docker/compose.py` — compose generation & commands -- `apps/api/src/services/docker/container.py` — container runtime queries -- `apps/api/src/services/docker/config_staging.py` — file staging into instances -- `apps/api/src/services/docker/tunnel.py` — cloudflared tunnel lifecycle -- `apps/api/src/services/tunnel.py` — thin backward-compat re-export wrapper ## Notes -- All backward-compatible imports preserved via __init__.py re-exports -- `from src.services.docker import X` continues to work for all previously exported symbols -- `from src.services.tunnel import X` continues to work via re-export wrapper -- No behavior changes, pure structural refactor