From eadbba9362416d369b8689503dedadf65eed9229 Mon Sep 17 00:00:00 2001 From: Artem Sviridov Date: Fri, 11 Apr 2025 15:41:34 +0300 Subject: [PATCH 1/3] ID27113: estimation time callback --- webapplication/aoi/serializers.py | 17 ++++++++++++++++- webapplication/aoi/urls.py | 4 ++-- webapplication/aoi/views.py | 6 +++--- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/webapplication/aoi/serializers.py b/webapplication/aoi/serializers.py index d931953a..e20a1b8a 100644 --- a/webapplication/aoi/serializers.py +++ b/webapplication/aoi/serializers.py @@ -48,6 +48,8 @@ class RequestSerializer(serializers.ModelSerializer): notebook = serializers.PrimaryKeyRelatedField(source='component', many=False, queryset=Component.objects, label="Component id") + finished_in = serializers.DurationField(write_only=True, required=False) + def create(self, validated_data): if validated_data.get("aoi"): validated_data.update({'polygon': validated_data["aoi"].polygon}) @@ -65,6 +67,10 @@ def validate(self, attrs): - If chosen notebook that require period then date_from and date_to in request are required as well """ + request = self.context.get("request") + if request and request.method == "PATCH": + return super().validate(attrs) + if attrs['component'].period_required and (not 'date_from' in attrs or not 'date_to' in attrs): exception_details = {} if not 'date_from' in attrs: @@ -76,6 +82,14 @@ def validate(self, attrs): raise serializers.ValidationError(exception_details) return super().validate(attrs) + def update(self, instance, validated_data): + finished_in = validated_data.pop("finished_in", None) + if finished_in: + instance.estimated_finish_time = timezone.now() + finished_in + instance.save() + return instance + + def to_representation(self, instance): data = super().to_representation(instance) if instance.estimated_finish_time: @@ -90,4 +104,5 @@ class Meta: model = Request fields = ('id', 'user', 'aoi', 'notebook', 'notebook_name', 'date_from', 'date_to', 'started_at', 'finished_at', 'error', 'calculated', 'success', 'polygon', - 'additional_parameter', 'additional_parameter2', 'pre_submit', 'request_origin', 'user_readable_errors') + 'additional_parameter', 'additional_parameter2', 'pre_submit', 'request_origin', + 'user_readable_errors', 'estimated_finish_time', 'finished_in') diff --git a/webapplication/aoi/urls.py b/webapplication/aoi/urls.py index 7d03257f..a96a7290 100644 --- a/webapplication/aoi/urls.py +++ b/webapplication/aoi/urls.py @@ -1,7 +1,7 @@ from django.urls import path from .views import (AoIListCreateAPIView, AoIRetrieveUpdateDestroyAPIView, AOIResultsListAPIView, ComponentListCreateAPIView, ComponentRetrieveUpdateDestroyAPIView, - RequestListCreateAPIView, RequestRetrieveAPIView, AOIRequestListAPIView) + RequestListCreateAPIView, RequestRetrieveUpdateAPIView, AOIRequestListAPIView) app_name = 'aoi' urlpatterns = [ @@ -14,5 +14,5 @@ path('notebook/', ComponentRetrieveUpdateDestroyAPIView.as_view(), name='notebook'), path('request', RequestListCreateAPIView.as_view(), name='request_list_or_create'), - path('request/', RequestRetrieveAPIView.as_view(), name='request'), + path('request/', RequestRetrieveUpdateAPIView.as_view(), name='request'), ] diff --git a/webapplication/aoi/views.py b/webapplication/aoi/views.py index 0f97063b..428dba60 100644 --- a/webapplication/aoi/views.py +++ b/webapplication/aoi/views.py @@ -5,7 +5,7 @@ from rest_framework import status from rest_framework.serializers import ValidationError, as_serializer_error from rest_framework.response import Response -from rest_framework.generics import ListAPIView, RetrieveUpdateDestroyAPIView, ListCreateAPIView, RetrieveAPIView +from rest_framework.generics import ListAPIView, RetrieveUpdateDestroyAPIView, ListCreateAPIView, RetrieveUpdateAPIView from rest_framework.generics import get_object_or_404 from publisher.serializers import ResultSerializer from publisher.models import Result @@ -237,7 +237,7 @@ def create(self, request, *args, **kwargs): return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) -class RequestRetrieveAPIView(RetrieveAPIView): +class RequestRetrieveUpdateAPIView(RetrieveUpdateAPIView): """ Reads Request fields. Accepts: GET method. @@ -249,7 +249,7 @@ class RequestRetrieveAPIView(RetrieveAPIView): permission_classes = (ModelPermissions, IsOwnerPermission) queryset = Request.objects.all() serializer_class = RequestSerializer - http_method_names = ("get", ) + http_method_names = ("get", "patch") class AOIRequestListAPIView(ListAPIView): From 208071272094688eb635950cc082133dfbf51b96 Mon Sep 17 00:00:00 2001 From: Artem Sviridov Date: Fri, 11 Apr 2025 16:43:53 +0300 Subject: [PATCH 2/3] ID27113: estimation time callback geoap client fix --- components/remote_executor/geoapp_client.py | 29 +++++++++++++++++++++ webapplication/aoi/permissions.py | 4 +++ webapplication/aoi/views.py | 4 +-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/components/remote_executor/geoapp_client.py b/components/remote_executor/geoapp_client.py index 8d9fb54c..75626d23 100644 --- a/components/remote_executor/geoapp_client.py +++ b/components/remote_executor/geoapp_client.py @@ -4,10 +4,13 @@ import time import os from urllib3.util.retry import Retry +from datetime import datetime, timezone, timedelta RETRY_INTERVAL = 2880 # num of intervals -> every minute RETRY_LIMIT_SECONDS = 172800.0 # 2 days in seconds REQUEST_TIMEOUT = 10 +base_url = "https://portal.soilmate.ai" +token = "" class GeoappClient: @@ -33,9 +36,29 @@ def __init__(self, geoap_creds): ) self.api_endpoint = geoap_creds_data["API_ENDPOINT"] + self.main_domain = geoap_creds_data["MAIN_DOMAIN"] + self.main_key = geoap_creds_data["MAIN_KEY"] self.http = http self.log = log + def update_estimated_finish_time(self, duration): + request_id = os.getenv('REQUEST_ID', 980) + try: + response = self.http.request( + "PATCH", + f"{self.main_domain}/api/request/{request_id}", + body=json.dumps({"finished_in": duration}), + headers={ + "Authorization": f"Token {self.main_key}", + "Content-Type": "application/json" + }) + if response.status == 200: + self.log.info(f"Request with time update sent, duration: {duration}, request_id: {request_id}") + else: + self.log.info(f"Request with time update failed, {response.status}") + except urllib3.exceptions.RequestError as e: + self.log.info(f"Request failed: {e}") + def get_component_id(self, name): url = self.api_endpoint + "api/notebook" response = self.http.request("GET", url) @@ -77,6 +100,12 @@ def wait_for_request_success_finish(self, request_id): while time.time() < start_time + RETRY_LIMIT_SECONDS: response = self.http.request("GET", url) curr_request = json.loads(response.data.decode()) + if curr_request.get("estimated_finish_time"): + self.log.info(f"Estimated finish time: {curr_request.get('estimated_finish_time')}") + estimated_finish_dt = datetime.fromisoformat(curr_request.get("estimated_finish_time") + .rstrip("Z")).replace(tzinfo=timezone.utc) + duration = (estimated_finish_dt - datetime.now(timezone.utc)).total_seconds() + self.update_estimated_finish_time(str(timedelta(seconds=duration))) if curr_request.get("finished_at"): if not curr_request.get("calculated"): return False, curr_request.get("error") diff --git a/webapplication/aoi/permissions.py b/webapplication/aoi/permissions.py index f0d43246..83db7731 100644 --- a/webapplication/aoi/permissions.py +++ b/webapplication/aoi/permissions.py @@ -4,3 +4,7 @@ class AoIIsOwnerPermission(BasePermission): def has_object_permission(self, request, view, obj): return obj.user == request.user + +class IsAdminUserOverride(BasePermission): + def has_permission(self, request, view): + return request.user and request.user.is_staff diff --git a/webapplication/aoi/views.py b/webapplication/aoi/views.py index 428dba60..730b6406 100644 --- a/webapplication/aoi/views.py +++ b/webapplication/aoi/views.py @@ -13,7 +13,7 @@ from .models import AoI, Component, Request from .serializers import AoISerializer, ComponentSerializer, RequestSerializer from user.permissions import ModelPermissions, IsOwnerPermission -from .permissions import AoIIsOwnerPermission +from .permissions import AoIIsOwnerPermission, IsAdminUserOverride from user.models import User, Transaction from allauth.account import app_settings from allauth.utils import build_absolute_uri @@ -246,7 +246,7 @@ class RequestRetrieveUpdateAPIView(RetrieveUpdateAPIView): 'finished_at', 'error', 'calculated', 'success', 'polygon', 'additional_parameter'. Returns: RequestModel fields. """ - permission_classes = (ModelPermissions, IsOwnerPermission) + permission_classes = (IsAdminUserOverride | (ModelPermissions & IsOwnerPermission),) queryset = Request.objects.all() serializer_class = RequestSerializer http_method_names = ("get", "patch") From d22708dc04ff7ed5b4849f934826ddce5fa3a6de Mon Sep 17 00:00:00 2001 From: Artem Sviridov Date: Fri, 11 Apr 2025 17:35:12 +0300 Subject: [PATCH 3/3] ID27113: style fix --- components/remote_executor/geoapp_client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/remote_executor/geoapp_client.py b/components/remote_executor/geoapp_client.py index 75626d23..c2d35d8f 100644 --- a/components/remote_executor/geoapp_client.py +++ b/components/remote_executor/geoapp_client.py @@ -9,8 +9,6 @@ RETRY_INTERVAL = 2880 # num of intervals -> every minute RETRY_LIMIT_SECONDS = 172800.0 # 2 days in seconds REQUEST_TIMEOUT = 10 -base_url = "https://portal.soilmate.ai" -token = "" class GeoappClient: