Compare commits

..

No commits in common. "3a7973accdb0895f2a33794d57451e641e6d58e3" and "bae540427a280afe5efd28196eba8fba9fce666b" have entirely different histories.

11 changed files with 21 additions and 108 deletions

View file

@ -8,7 +8,6 @@ from dotenv import dotenv_values
from .model import (
AppConfig,
AppSectionConfig,
DockerConfig,
HttpConfig,
LoggingConfig,
MetricsConfig,
@ -40,7 +39,6 @@ def load_config(
logging_section = _section(yaml_data, 'logging')
metrics_section = _section(yaml_data, 'metrics')
tracing_section = _section(yaml_data, 'tracing')
docker_section = _section(yaml_data, 'docker')
sandbox_section = _section(yaml_data, 'sandbox')
security_section = _section(yaml_data, 'security')
@ -132,15 +130,6 @@ def load_config(
enable_metrics=metrics_enabled,
enable_tracing=tracing_enabled,
),
docker=DockerConfig(
base_url=_yaml_or_env_str(
docker_section,
'base_url',
'docker.base_url',
env_values,
'APP_DOCKER_BASE_URL',
)
),
sandbox=_load_sandbox_config(sandbox_section, env_values),
security=SecurityConfig(
token_header=_yaml_or_env_str(

View file

@ -40,11 +40,6 @@ class OtelConfig:
metric_export_interval: int
@dataclass(frozen=True, slots=True)
class DockerConfig:
base_url: str
@dataclass(frozen=True, slots=True)
class SandboxConfig:
image: str
@ -73,6 +68,5 @@ class AppConfig:
metrics: MetricsConfig
tracing: TracingConfig
otel: OtelConfig
docker: DockerConfig
sandbox: SandboxConfig
security: SecurityConfig

View file

@ -78,8 +78,8 @@ def build_container(
)
observability = build_observability(app_config)
docker_client: DockerClient = docker.from_env()
clock = SystemClock()
docker_client = docker.DockerClient(base_url=app_config.docker.base_url)
user_repository = InMemoryUserRepository(
observability.tracer, [User(id='123', email='aza@gglamer.ru', name='gglamer')]

View file

@ -1,3 +1,4 @@
from collections.abc import Callable
from datetime import datetime
from pathlib import Path
@ -10,22 +11,25 @@ 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:
@ -55,7 +59,7 @@ class DockerSandboxRuntime(SandboxRuntime):
chat_id=chat_id,
container_id=container_id,
status=SandboxStatus.RUNNING,
created_at=created_at,
created_at=self._now(expires_at),
expires_at=expires_at,
)
@ -124,3 +128,7 @@ 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)

View file

@ -1,7 +1,8 @@
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'
@ -10,16 +11,10 @@ APP_CONFIG_STATE = 'config'
def get_container(request: Request) -> AppContainer:
container = getattr(request.app.state, APP_CONTAINER_STATE, None)
if not isinstance(container, AppContainer):
if container is None:
raise RuntimeError('container unavailable')
return container
return cast(AppContainer, 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

View file

@ -1,21 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, status
from adapter.di.container import AppContainer
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 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 usecase.user import GetUser, GetUserQuery
router = APIRouter()
@ -50,42 +38,3 @@ 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,
)

View file

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

View file

@ -24,9 +24,6 @@ otel:
traces_endpoint: http://localhost:4318/v1/traces
metric_export_interval: 1000
docker:
base_url: unix:///var/run/docker.sock
sandbox:
image: ai-agent:latest
ttl_seconds: 300

View file

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

View file

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

View file

@ -53,12 +53,10 @@ 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,
created_at=now,
expires_at=expires_at,
expires_at=now + self._ttl,
)
self._repository.save(new_session)
self._logger.info(