diff --git a/adapter/docker/runtime.py b/adapter/docker/runtime.py index 89df9ab..61fcaf6 100644 --- a/adapter/docker/runtime.py +++ b/adapter/docker/runtime.py @@ -1,4 +1,3 @@ -from collections.abc import Callable from datetime import datetime from pathlib import Path @@ -11,25 +10,22 @@ from domain.error import SandboxError, SandboxStartError from domain.sandbox import SandboxSession, SandboxStatus from usecase.interface import SandboxRuntime -type NowFactory = Callable[[datetime], datetime] - class DockerSandboxRuntime(SandboxRuntime): def __init__( self, config: SandboxConfig, client: DockerClient, - now: NowFactory | None = None, ) -> None: self._config = config self._client = client - self._now = _current_time if now is None else now def create( self, *, session_id: str, chat_id: str, + created_at: datetime, expires_at: datetime, ) -> SandboxSession: try: @@ -59,7 +55,7 @@ class DockerSandboxRuntime(SandboxRuntime): chat_id=chat_id, container_id=container_id, status=SandboxStatus.RUNNING, - created_at=self._now(expires_at), + created_at=created_at, expires_at=expires_at, ) @@ -128,7 +124,3 @@ class DockerSandboxRuntime(SandboxRuntime): def _host_path(self, path_value: str) -> Path: return Path(path_value).expanduser().resolve(strict=False) - - -def _current_time(expires_at: datetime) -> datetime: - return datetime.now(tz=expires_at.tzinfo) diff --git a/adapter/http/fastapi/dependencies.py b/adapter/http/fastapi/dependencies.py index 4892459..87a9224 100644 --- a/adapter/http/fastapi/dependencies.py +++ b/adapter/http/fastapi/dependencies.py @@ -1,8 +1,7 @@ -from typing import cast - from fastapi import Depends, Request from adapter.di.container import AppContainer +from usecase.sandbox import CreateSandbox from usecase.user import GetUser APP_CONTAINER_STATE = 'container' @@ -11,10 +10,16 @@ APP_CONFIG_STATE = 'config' def get_container(request: Request) -> AppContainer: container = getattr(request.app.state, APP_CONTAINER_STATE, None) - if container is None: + if not isinstance(container, AppContainer): raise RuntimeError('container unavailable') - return cast(AppContainer, container) + return container def get_get_user(container: AppContainer = Depends(get_container)) -> GetUser: return container.usecases.get_user + + +def get_create_sandbox( + container: AppContainer = Depends(get_container), +) -> CreateSandbox: + return container.usecases.create_sandbox diff --git a/adapter/http/fastapi/routers/v1/router.py b/adapter/http/fastapi/routers/v1/router.py index df3d575..1f0aff4 100644 --- a/adapter/http/fastapi/routers/v1/router.py +++ b/adapter/http/fastapi/routers/v1/router.py @@ -1,9 +1,21 @@ from fastapi import APIRouter, Depends, HTTPException, status from adapter.di.container import AppContainer -from adapter.http.fastapi.dependencies import get_container, get_get_user -from adapter.http.fastapi.schemas import ErrorResponse, HealthResponse, UserResponse -from domain.error import UserNotFoundError +from adapter.http.fastapi.dependencies import ( + get_container, + get_create_sandbox, + get_get_user, +) +from adapter.http.fastapi.schemas import ( + CreateSandboxRequest, + ErrorResponse, + HealthResponse, + SandboxSessionResponse, + UserResponse, +) +from domain.error import SandboxError, SandboxStartError, UserNotFoundError +from domain.sandbox import SandboxSession +from usecase.sandbox import CreateSandbox, CreateSandboxCommand from usecase.user import GetUser, GetUserQuery router = APIRouter() @@ -38,3 +50,42 @@ def get_user(user_id: str, usecase: GetUser = Depends(get_get_user)) -> UserResp ) from exc return UserResponse(id=user.id, email=user.email, name=user.name) + + +@router.post( + '/create', + response_model=SandboxSessionResponse, + responses={ + status.HTTP_503_SERVICE_UNAVAILABLE: {'model': ErrorResponse}, + status.HTTP_500_INTERNAL_SERVER_ERROR: {'model': ErrorResponse}, + }, + status_code=status.HTTP_200_OK, +) +def create_sandbox( + request: CreateSandboxRequest, + usecase: CreateSandbox = Depends(get_create_sandbox), +) -> SandboxSessionResponse: + try: + session = usecase.execute(CreateSandboxCommand(chat_id=request.chat_id)) + except SandboxStartError as exc: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(exc), + ) from exc + except SandboxError as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(exc), + ) from exc + + return _to_sandbox_session_response(session) + + +def _to_sandbox_session_response(session: SandboxSession) -> SandboxSessionResponse: + return SandboxSessionResponse( + session_id=session.session_id, + chat_id=session.chat_id, + container_id=session.container_id, + status=session.status.value, + expires_at=session.expires_at, + ) diff --git a/adapter/http/fastapi/schemas.py b/adapter/http/fastapi/schemas.py index e11c95b..08d9056 100644 --- a/adapter/http/fastapi/schemas.py +++ b/adapter/http/fastapi/schemas.py @@ -1,4 +1,6 @@ -from pydantic import BaseModel +from datetime import datetime + +from pydantic import BaseModel, ConfigDict, Field class HealthResponse(BaseModel): @@ -7,6 +9,20 @@ class HealthResponse(BaseModel): env: str +class CreateSandboxRequest(BaseModel): + model_config = ConfigDict(extra='forbid', str_strip_whitespace=True) + + chat_id: str = Field(min_length=1) + + +class SandboxSessionResponse(BaseModel): + session_id: str + chat_id: str + container_id: str + status: str + expires_at: datetime + + class UserResponse(BaseModel): id: str email: str diff --git a/tasks.md b/tasks.md index e8c293c..49c99a6 100644 --- a/tasks.md +++ b/tasks.md @@ -86,7 +86,7 @@ ### M06. HTTP endpoint `POST /api/v1/create` - Субагент: `feature-developer` -- Статус: pending +- Статус: completed - Зависимости: `M04` - Commit required: no - Scope: добавить минимальную HTTP ручку для создания или переиспользования sandbox без auth diff --git a/usecase/interface.py b/usecase/interface.py index fcc1fe6..0c8bcaa 100644 --- a/usecase/interface.py +++ b/usecase/interface.py @@ -34,6 +34,7 @@ class SandboxRuntime(Protocol): *, session_id: str, chat_id: str, + created_at: datetime, expires_at: datetime, ) -> SandboxSession: ... diff --git a/usecase/sandbox.py b/usecase/sandbox.py index ae60946..65740ef 100644 --- a/usecase/sandbox.py +++ b/usecase/sandbox.py @@ -53,10 +53,12 @@ class CreateSandbox: self._runtime.stop(session.container_id) self._repository.delete(session.session_id) + expires_at = now + self._ttl new_session = self._runtime.create( session_id=_new_session_id(), chat_id=command.chat_id, - expires_at=now + self._ttl, + created_at=now, + expires_at=expires_at, ) self._repository.save(new_session) self._logger.info(