This commit is contained in:
2026-03-17 18:32:44 +03:00
commit efcd4a8dfd
209 changed files with 33355 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
from app.api.actions.router import router
__all__ = ["router"]
+49
View File
@@ -0,0 +1,49 @@
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.actions.dependencies import get_active_action_or_404
from app.core.database.session import get_session
from app.models import User
from app.utils.business_logger import log_business_event
from app.utils.token_manager import get_current_user
router = APIRouter(tags=["Actions"])
@router.delete("/{action_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_action(
action_id: UUID,
request: Request,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_user),
):
trace_id = getattr(request.state, "traceId", None)
try:
action = await get_active_action_or_404(session, action_id, current_user)
except HTTPException:
log_business_event(
"action_delete_rejected",
trace_id=trace_id,
user_id=str(current_user.id),
action_id=str(action_id),
reason="action_not_found_or_forbidden",
)
raise
action.is_deleted = True
await session.commit()
await session.refresh(action)
log_business_event(
"action_deleted",
trace_id=trace_id,
user_id=str(current_user.id),
action_id=str(action.id),
)
return Response(status_code=status.HTTP_204_NO_CONTENT)
+21
View File
@@ -0,0 +1,21 @@
from __future__ import annotations
from uuid import UUID
from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import Action, ActionIngestStatus, User, UserRole
async def get_active_action_or_404(
session: AsyncSession,
action_id: UUID,
current_user: User,
) -> Action:
action = await session.get(Action, action_id)
if action is None or action.is_deleted or action.ingest_status != ActionIngestStatus.SUCCEEDED:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Action not found")
if current_user.role != UserRole.ADMIN and action.user_id != current_user.id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Action not found")
return action
+47
View File
@@ -0,0 +1,47 @@
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.actions.dependencies import get_active_action_or_404
from app.core.database.session import get_session
from app.models import User
from app.schemas.action_sch import ActionDetailResponse
from app.utils.business_logger import log_business_event
from app.utils.token_manager import get_current_user
router = APIRouter(tags=["Actions"])
@router.get("/{action_id}", response_model=ActionDetailResponse)
async def get_action(
action_id: UUID,
request: Request,
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_user),
):
trace_id = getattr(request.state, "traceId", None)
try:
action = await get_active_action_or_404(session, action_id, current_user)
except HTTPException:
log_business_event(
"action_fetch_rejected",
trace_id=trace_id,
user_id=str(current_user.id),
action_id=str(action_id),
reason="action_not_found_or_forbidden",
)
raise
log_business_event(
"action_fetched",
trace_id=trace_id,
user_id=str(current_user.id),
action_id=str(action.id),
action_method=action.method.value if action.method is not None else None,
action_path=action.path,
)
return action
+92
View File
@@ -0,0 +1,92 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database.session import get_session
from app.models import Action, ActionIngestStatus, User
from app.schemas.capability_sch import ActionIngestWithCapabilitiesResponse
from app.services.capability_service import CapabilityService
from app.services.openapi_service import OpenAPIService
from app.utils.business_logger import log_business_event
from app.utils.token_manager import get_current_user
router = APIRouter(tags=["Actions"])
@router.post("/ingest", response_model=ActionIngestWithCapabilitiesResponse, status_code=status.HTTP_201_CREATED)
async def ingest_actions(
request: Request,
file: UploadFile = File(...),
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_user),
):
trace_id = getattr(request.state, "traceId", None)
payload = await file.read()
try:
document = OpenAPIService.load_document(payload)
ingestion_result = OpenAPIService.extract_actions_with_failures(document, source_filename=file.filename)
except ValueError as exc:
log_business_event(
"actions_ingest_rejected",
trace_id=trace_id,
user_id=str(current_user.id),
source_filename=file.filename,
file_size_bytes=len(payload),
reason="invalid_openapi_document",
details=str(exc),
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
action_payloads = ingestion_result["succeeded"] + ingestion_result["failed"]
if not action_payloads:
log_business_event(
"actions_ingest_rejected",
trace_id=trace_id,
user_id=str(current_user.id),
source_filename=file.filename,
file_size_bytes=len(payload),
reason="no_supported_operations",
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No supported HTTP operations found in OpenAPI file")
actions = [Action(user_id=current_user.id, **action_payload) for action_payload in action_payloads]
session.add_all(actions)
await session.flush()
succeeded_actions = [action for action in actions if action.ingest_status == ActionIngestStatus.SUCCEEDED]
failed_actions = [action for action in actions if action.ingest_status == ActionIngestStatus.FAILED]
capability_service = CapabilityService(session)
capabilities = await capability_service.create_from_actions(
succeeded_actions,
owner_user_id=current_user.id,
refresh=False,
)
await session.commit()
for action in actions:
await session.refresh(action)
for capability in capabilities:
await session.refresh(capability)
log_business_event(
"actions_ingested",
trace_id=trace_id,
user_id=str(current_user.id),
source_filename=file.filename,
file_size_bytes=len(payload),
succeeded_count=len(succeeded_actions),
failed_count=len(failed_actions),
created_capabilities_count=len(capabilities),
)
return ActionIngestWithCapabilitiesResponse(
succeeded_count=len(succeeded_actions),
failed_count=len(failed_actions),
created_capabilities_count=len(capabilities),
succeeded_actions=succeeded_actions,
failed_actions=failed_actions,
capabilities=capabilities,
)
+79
View File
@@ -0,0 +1,79 @@
from __future__ import annotations
from uuid import UUID
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database.session import get_session
from app.models import Action, ActionIngestStatus, HttpMethod, User, UserRole
from app.schemas.action_sch import ActionListItemResponse
from app.utils.business_logger import log_business_event
from app.utils.token_manager import get_current_user
router = APIRouter(tags=["Actions"])
@router.get("/", response_model=list[ActionListItemResponse], include_in_schema=False)
async def list_actions(
request: Request,
method: HttpMethod | None = Query(default=None),
owner_id: UUID | None = Query(default=None),
source_filename: str | None = Query(default=None),
search: str | None = Query(default=None, min_length=1),
limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0),
session: AsyncSession = Depends(get_session),
current_user: User = Depends(get_current_user),
):
trace_id = getattr(request.state, "traceId", None)
query = (
select(Action)
.where(Action.is_deleted.is_(False))
.where(Action.ingest_status == ActionIngestStatus.SUCCEEDED)
.order_by(Action.created_at.desc())
.limit(limit)
.offset(offset)
)
if current_user.role == UserRole.ADMIN:
if owner_id is not None:
query = query.where(Action.user_id == owner_id)
else:
query = query.where(Action.user_id == current_user.id)
if method is not None:
query = query.where(Action.method == method)
if source_filename:
query = query.where(Action.source_filename == source_filename)
if search:
search_pattern = f"%{search}%"
query = query.where(
or_(
Action.operation_id.ilike(search_pattern),
Action.path.ilike(search_pattern),
Action.summary.ilike(search_pattern),
)
)
result = await session.execute(query)
actions = list(result.scalars().all())
log_business_event(
"actions_listed",
trace_id=trace_id,
user_id=str(current_user.id),
method=method.value if method is not None else None,
owner_id=str(owner_id) if owner_id is not None else None,
source_filename=source_filename,
search=search,
limit=limit,
offset=offset,
result_count=len(actions),
)
return actions
+13
View File
@@ -0,0 +1,13 @@
from fastapi import APIRouter
from app.api.actions.delete_action import router as delete_action_router
from app.api.actions.get_action import router as get_action_router
from app.api.actions.ingest_actions import router as ingest_actions_router
from app.api.actions.list_actions import router as list_actions_router
router = APIRouter(prefix="/v1/actions", tags=["Actions"])
router.include_router(ingest_actions_router)
router.include_router(list_actions_router)
router.include_router(get_action_router)
router.include_router(delete_action_router)