RamEx / api /views /analysis_unified_views_first.py
zdy10046's picture
deploy RamEx to Hugging Face without binary files
e657e99
Raw
History Blame Contribute Delete
11.7 kB
import json
import os
import zipfile
from django.conf import settings
from django.http import FileResponse, Http404
from django.utils import timezone
from rest_framework import permissions, status
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView
from api.models import AnalysisTask, Project
from api.serializers import AnalysisTaskSerializer
from api.utils.r_executor import RExecutor
class _AnalysisViewMixin:
permission_classes = [permissions.IsAuthenticated]
DATA_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tif", ".tiff", ".svg", ".pdf"}
@staticmethod
def _get_project(request, project_id):
return Project.objects.get(id=project_id, user=request.user)
@staticmethod
def _get_project_media_dir(user_id, project_id):
return os.path.join(settings.MEDIA_ROOT, f"users/{user_id}/projects/{project_id}")
@staticmethod
def _get_results_dir(user_id, project_id):
return os.path.join(settings.MEDIA_ROOT, f"users/{user_id}/projects/{project_id}/imgs/analysis")
@staticmethod
def _get_images_dir(user_id, project_id):
return os.path.join(settings.MEDIA_ROOT, f"users/{user_id}/projects/{project_id}/imgs")
@staticmethod
def _get_temp_dir(user_id, project_id):
return os.path.join(settings.MEDIA_ROOT, f"users/{user_id}/projects/{project_id}/temp")
@staticmethod
def _parse_params(request):
params = request.data
if "params" in params and isinstance(params["params"], str):
params = json.loads(params["params"])
return dict(params)
@staticmethod
def _save_optional_uploads(request, project_id, user_id, params):
if "bands_csv" not in request.FILES:
return params
file_path = os.path.join(
settings.MEDIA_ROOT,
f"users/{user_id}/projects/{project_id}/data/bands_ann.csv",
)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "wb+") as destination:
for chunk in request.FILES["bands_csv"].chunks():
destination.write(chunk)
params["bands_csv_path"] = file_path
return params
@staticmethod
def _build_empty_result():
return {"status": "success", "analyses": {}, "plots": {}, "artifacts": {}}
@classmethod
def _should_export_file(cls, project_media_dir, file_path, temp_dir):
rel_path = os.path.relpath(file_path, project_media_dir)
normalized_rel_path = rel_path.replace(os.sep, "/")
if os.path.normpath(file_path).startswith(os.path.normpath(temp_dir)):
return False
# RamEx functions often create plots in the R working directory first.
# The executor copies those plots into imgs/ for preview, so exporting
# image files from data/ creates duplicate images in the result zip.
if normalized_rel_path.startswith("data/"):
_, ext = os.path.splitext(file_path)
if ext.lower() in cls.DATA_IMAGE_EXTENSIONS:
return False
return True
class AnalysisStatusView(_AnalysisViewMixin, APIView):
def get(self, request, project_id):
try:
project = self._get_project(request, project_id)
except Project.DoesNotExist:
return Response(
{"error": {"code": "project_not_found", "message": "项目不存在"}},
status=status.HTTP_404_NOT_FOUND,
)
task = AnalysisTask.objects.filter(project=project).order_by("-created_at").first()
if not task:
return Response({"status": "not_started"}, status=status.HTTP_200_OK)
serializer = AnalysisTaskSerializer(task)
return Response(serializer.data, status=status.HTTP_200_OK)
class AnalysisTaskView(_AnalysisViewMixin, APIView):
parser_classes = [JSONParser, MultiPartParser, FormParser]
def post(self, request, project_id):
try:
project = self._get_project(request, project_id)
except Project.DoesNotExist:
return Response(
{"error": {"code": "project_not_found", "message": "项目不存在"}},
status=status.HTTP_404_NOT_FOUND,
)
if project.status not in ["data_loaded", "preprocessing_completed", "analysis", "completed"]:
return Response(
{"error": {"code": "invalid_status", "message": "项目当前状态不允许执行分析"}},
status=status.HTTP_400_BAD_REQUEST,
)
try:
params = self._parse_params(request)
except json.JSONDecodeError:
return Response(
{"error": {"code": "invalid_params", "message": "参数格式无效"}},
status=status.HTTP_400_BAD_REQUEST,
)
params = self._save_optional_uploads(request, project_id, request.user.id, params)
if not RExecutor.has_selected_work(params):
return Response(
{"error": {"code": "no_method_selected", "message": "请至少选择一种需要执行的算法或处理步骤"}},
status=status.HTTP_400_BAD_REQUEST,
)
task = AnalysisTask.objects.create(
project=project,
analysis_type=RExecutor.infer_analysis_type(params),
parameters=params,
status="processing",
)
project.status = "analysis"
project.save()
user_id = str(request.user.id)
results_dir = self._get_results_dir(user_id, project_id)
os.makedirs(results_dir, exist_ok=True)
with open(os.path.join(results_dir, "analysis_params.json"), "w", encoding="utf-8") as file_obj:
json.dump(params, file_obj, ensure_ascii=False, indent=4)
try:
result = RExecutor.execute_analysis(project_id=str(project.id), params=params)
if result.get("status") == "error":
raise RuntimeError(result.get("message", "分析执行失败"))
with open(os.path.join(results_dir, "analysis_results.json"), "w", encoding="utf-8") as file_obj:
json.dump(result, file_obj, ensure_ascii=False, indent=4)
task.status = "completed"
task.result = result
task.completed_at = timezone.now()
task.save()
project.status = "completed"
project.save()
return Response(
{"status": "success", "message": "分析完成", "result": result},
status=status.HTTP_200_OK,
)
except Exception as exc:
task.status = "error"
task.result = {"error": str(exc)}
task.completed_at = timezone.now()
task.save()
project.status = "error"
project.save()
return Response(
{"status": "error", "error": {"message": str(exc)}},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
class AnalysisResultsView(_AnalysisViewMixin, APIView):
def get(self, request, project_id):
try:
project = self._get_project(request, project_id)
except Project.DoesNotExist:
return Response(
{"error": {"code": "project_not_found", "message": "项目不存在"}},
status=status.HTTP_404_NOT_FOUND,
)
user_id = str(request.user.id)
results_dir = self._get_results_dir(user_id, project_id)
results_file = os.path.join(results_dir, "analysis_results.json")
params_file = os.path.join(results_dir, "analysis_params.json")
if os.path.exists(results_file):
try:
with open(results_file, "r", encoding="utf-8") as file_obj:
saved_results = json.load(file_obj)
if os.path.exists(params_file) and "parameters" not in saved_results:
with open(params_file, "r", encoding="utf-8") as file_obj:
saved_results["parameters"] = json.load(file_obj)
return Response({"status": "success", "result": saved_results}, status=status.HTTP_200_OK)
except Exception:
pass
task = (
AnalysisTask.objects.filter(project=project, status="completed")
.order_by("-completed_at")
.first()
)
if task:
result = task.result or self._build_empty_result()
if isinstance(result, dict) and "parameters" not in result:
result["parameters"] = task.parameters
return Response({"status": "success", "result": result}, status=status.HTTP_200_OK)
if AnalysisTask.objects.filter(project=project, status="processing").exists():
return Response(
{"status": "processing", "message": "分析任务正在进行中"},
status=status.HTTP_200_OK,
)
return Response(
{
"status": "success",
"message": "没有找到已完成的分析结果",
"result": self._build_empty_result(),
},
status=status.HTTP_200_OK,
)
class AnalysisExportView(_AnalysisViewMixin, APIView):
def get(self, request, project_id):
try:
project = self._get_project(request, project_id)
except Project.DoesNotExist:
return Response(
{"error": {"code": "project_not_found", "message": "项目不存在"}},
status=status.HTTP_404_NOT_FOUND,
)
user_id = str(request.user.id)
results_dir = self._get_results_dir(user_id, project_id)
images_dir = self._get_images_dir(user_id, project_id)
temp_dir = self._get_temp_dir(user_id, project_id)
has_results = os.path.exists(results_dir) and any(
os.path.isfile(os.path.join(results_dir, name)) for name in os.listdir(results_dir)
)
has_images = os.path.exists(images_dir) and any(
os.path.isfile(os.path.join(root, name))
for root, _, files in os.walk(images_dir)
for name in files
)
if not has_results and not has_images:
return Response(
{"error": {"code": "no_results", "message": "没有找到可导出的分析结果"}},
status=status.HTTP_404_NOT_FOUND,
)
os.makedirs(temp_dir, exist_ok=True)
zip_filename = f"analysis_results_{project.name}_{timezone.now().strftime('%Y%m%d%H%M%S')}.zip"
zip_filepath = os.path.join(temp_dir, zip_filename)
with zipfile.ZipFile(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zip_file:
project_media_dir = self._get_project_media_dir(user_id, project_id)
for root, _, files in os.walk(project_media_dir):
for file_name in files:
file_path = os.path.join(root, file_name)
if not self._should_export_file(project_media_dir, file_path, temp_dir):
continue
arcname = os.path.relpath(file_path, project_media_dir)
zip_file.write(file_path, arcname)
if os.path.exists(zip_filepath):
response = FileResponse(open(zip_filepath, "rb"), as_attachment=True, filename=zip_filename)
response["Content-Type"] = "application/zip"
response["X-Accel-Redirect"] = zip_filepath
return response
raise Http404("无法创建导出文件")