ref #8: [feat] add http endpoint

This commit is contained in:
Azamat 2026-04-02 13:41:41 +03:00
parent bae540427a
commit d2506e0c63
7 changed files with 87 additions and 20 deletions

View file

@ -1,4 +1,3 @@
from collections.abc import Callable
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@ -11,25 +10,22 @@ from domain.error import SandboxError, SandboxStartError
from domain.sandbox import SandboxSession, SandboxStatus from domain.sandbox import SandboxSession, SandboxStatus
from usecase.interface import SandboxRuntime from usecase.interface import SandboxRuntime
type NowFactory = Callable[[datetime], datetime]
class DockerSandboxRuntime(SandboxRuntime): class DockerSandboxRuntime(SandboxRuntime):
def __init__( def __init__(
self, self,
config: SandboxConfig, config: SandboxConfig,
client: DockerClient, client: DockerClient,
now: NowFactory | None = None,
) -> None: ) -> None:
self._config = config self._config = config
self._client = client self._client = client
self._now = _current_time if now is None else now
def create( def create(
self, self,
*, *,
session_id: str, session_id: str,
chat_id: str, chat_id: str,
created_at: datetime,
expires_at: datetime, expires_at: datetime,
) -> SandboxSession: ) -> SandboxSession:
try: try:
@ -59,7 +55,7 @@ class DockerSandboxRuntime(SandboxRuntime):
chat_id=chat_id, chat_id=chat_id,
container_id=container_id, container_id=container_id,
status=SandboxStatus.RUNNING, status=SandboxStatus.RUNNING,
created_at=self._now(expires_at), created_at=created_at,
expires_at=expires_at, expires_at=expires_at,
) )
@ -128,7 +124,3 @@ class DockerSandboxRuntime(SandboxRuntime):
def _host_path(self, path_value: str) -> Path: def _host_path(self, path_value: str) -> Path:
return Path(path_value).expanduser().resolve(strict=False) return Path(path_value).expanduser().resolve(strict=False)
def _current_time(expires_at: datetime) -> datetime:
return datetime.now(tz=expires_at.tzinfo)

View file

@ -1,8 +1,7 @@
from typing import cast
from fastapi import Depends, Request from fastapi import Depends, Request
from adapter.di.container import AppContainer from adapter.di.container import AppContainer
from usecase.sandbox import CreateSandbox
from usecase.user import GetUser from usecase.user import GetUser
APP_CONTAINER_STATE = 'container' APP_CONTAINER_STATE = 'container'
@ -11,10 +10,16 @@ APP_CONFIG_STATE = 'config'
def get_container(request: Request) -> AppContainer: def get_container(request: Request) -> AppContainer:
container = getattr(request.app.state, APP_CONTAINER_STATE, None) container = getattr(request.app.state, APP_CONTAINER_STATE, None)
if container is None: if not isinstance(container, AppContainer):
raise RuntimeError('container unavailable') raise RuntimeError('container unavailable')
return cast(AppContainer, container) return container
def get_get_user(container: AppContainer = Depends(get_container)) -> GetUser: def get_get_user(container: AppContainer = Depends(get_container)) -> GetUser:
return container.usecases.get_user return container.usecases.get_user
def get_create_sandbox(
container: AppContainer = Depends(get_container),
) -> CreateSandbox:
return container.usecases.create_sandbox

View file

@ -1,9 +1,21 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from adapter.di.container import AppContainer from adapter.di.container import AppContainer
from adapter.http.fastapi.dependencies import get_container, get_get_user from adapter.http.fastapi.dependencies import (
from adapter.http.fastapi.schemas import ErrorResponse, HealthResponse, UserResponse get_container,
from domain.error import UserNotFoundError 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 from usecase.user import GetUser, GetUserQuery
router = APIRouter() router = APIRouter()
@ -38,3 +50,42 @@ def get_user(user_id: str, usecase: GetUser = Depends(get_get_user)) -> UserResp
) from exc ) from exc
return UserResponse(id=user.id, email=user.email, name=user.name) 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,
)

View file

@ -1,4 +1,6 @@
from pydantic import BaseModel from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class HealthResponse(BaseModel): class HealthResponse(BaseModel):
@ -7,6 +9,20 @@ class HealthResponse(BaseModel):
env: str 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): class UserResponse(BaseModel):
id: str id: str
email: str email: str

View file

@ -86,7 +86,7 @@
### M06. HTTP endpoint `POST /api/v1/create` ### M06. HTTP endpoint `POST /api/v1/create`
- Субагент: `feature-developer` - Субагент: `feature-developer`
- Статус: pending - Статус: completed
- Зависимости: `M04` - Зависимости: `M04`
- Commit required: no - Commit required: no
- Scope: добавить минимальную HTTP ручку для создания или переиспользования sandbox без auth - Scope: добавить минимальную HTTP ручку для создания или переиспользования sandbox без auth

View file

@ -34,6 +34,7 @@ class SandboxRuntime(Protocol):
*, *,
session_id: str, session_id: str,
chat_id: str, chat_id: str,
created_at: datetime,
expires_at: datetime, expires_at: datetime,
) -> SandboxSession: ... ) -> SandboxSession: ...

View file

@ -53,10 +53,12 @@ class CreateSandbox:
self._runtime.stop(session.container_id) self._runtime.stop(session.container_id)
self._repository.delete(session.session_id) self._repository.delete(session.session_id)
expires_at = now + self._ttl
new_session = self._runtime.create( new_session = self._runtime.create(
session_id=_new_session_id(), session_id=_new_session_id(),
chat_id=command.chat_id, chat_id=command.chat_id,
expires_at=now + self._ttl, created_at=now,
expires_at=expires_at,
) )
self._repository.save(new_session) self._repository.save(new_session)
self._logger.info( self._logger.info(