Compare commits
No commits in common. "cc5187c3a1001508bea64d6c5f46a72de9a46507" and "456551cd4ebca048ee42271855e9e76794789775" have entirely different histories.
cc5187c3a1
...
456551cd4e
17 changed files with 24 additions and 282 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,30 +0,0 @@
|
|||
# Generated by Django 5.1.7 on 2025-04-05 10:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0002_prediction_user_userprediction_delete_todo'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='userprediction',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userprediction',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userprediction',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
import uuid
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
class User(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
|
|
@ -11,10 +11,10 @@ class Prediction(models.Model):
|
|||
result = models.JSONField()
|
||||
deleted_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
|
||||
|
||||
class UserPrediction(models.Model):
|
||||
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||
prediction = models.ForeignKey("Prediction", on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField()
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
prediction = models.ForeignKey(Prediction, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'prediction')
|
||||
|
|
|
|||
|
|
@ -1,86 +1,7 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Prediction
|
||||
from datetime import datetime
|
||||
from .validators import (
|
||||
validate_custom_curve, rate_clip,
|
||||
_rfc3339_to_timestamp, base64_to_curve
|
||||
)
|
||||
|
||||
class PredictionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Prediction
|
||||
fields = ['id', 'created_at', 'updated_at', 'result']
|
||||
|
||||
|
||||
|
||||
PROFILE_STANDARD = "standard_profile"
|
||||
PROFILE_FLOAT = "float"
|
||||
PROFILE_REVERSE = "reverse"
|
||||
PROFILE_CUSTOM = "custom"
|
||||
LATEST_DATASET_KEYWORD = "latest"
|
||||
SUPPORTED_PROFILES = [PROFILE_STANDARD, PROFILE_FLOAT, PROFILE_REVERSE, PROFILE_CUSTOM]
|
||||
|
||||
|
||||
class PredictionRequestSerializer(serializers.Serializer):
|
||||
launch_latitude = serializers.FloatField(min_value=-90, max_value=90)
|
||||
launch_longitude = serializers.FloatField(min_value=0, max_value=360)
|
||||
launch_datetime = serializers.DateTimeField()
|
||||
launch_altitude = serializers.FloatField(required=False)
|
||||
format = serializers.CharField(default="json")
|
||||
profile = serializers.ChoiceField(choices=SUPPORTED_PROFILES, default=PROFILE_STANDARD)
|
||||
dataset = serializers.CharField(default=LATEST_DATASET_KEYWORD)
|
||||
|
||||
# --- профиль-dependent поля ---
|
||||
ascent_rate = serializers.FloatField(required=False, min_value=0.01)
|
||||
descent_rate = serializers.FloatField(required=False, min_value=0.01)
|
||||
burst_altitude = serializers.FloatField(required=False)
|
||||
float_altitude = serializers.FloatField(required=False)
|
||||
stop_datetime = serializers.DateTimeField(required=False)
|
||||
ascent_curve = serializers.CharField(required=False)
|
||||
descent_curve = serializers.CharField(required=False)
|
||||
interpolate = serializers.BooleanField(required=False, default=False)
|
||||
|
||||
def validate(self, data):
|
||||
profile = data.get("profile", PROFILE_STANDARD)
|
||||
launch_alt = data.get("launch_altitude", 0)
|
||||
|
||||
if profile == PROFILE_STANDARD:
|
||||
if 'burst_altitude' not in data:
|
||||
raise serializers.ValidationError("burst_altitude is required for standard profile.")
|
||||
if data['burst_altitude'] <= launch_alt:
|
||||
raise serializers.ValidationError("burst_altitude must be greater than launch_altitude.")
|
||||
|
||||
elif profile == PROFILE_FLOAT:
|
||||
if 'float_altitude' not in data or data['float_altitude'] <= launch_alt:
|
||||
raise serializers.ValidationError("float_altitude must be greater than launch_altitude.")
|
||||
if 'stop_datetime' not in data or data['stop_datetime'] <= data['launch_datetime']:
|
||||
raise serializers.ValidationError("stop_datetime must be later than launch_datetime.")
|
||||
|
||||
elif profile == PROFILE_CUSTOM:
|
||||
if 'ascent_curve' not in data or not validate_custom_curve(data['ascent_curve']):
|
||||
raise serializers.ValidationError("Invalid ascent_curve.")
|
||||
if 'descent_curve' not in data or not validate_custom_curve(data['descent_curve']):
|
||||
raise serializers.ValidationError("Invalid descent_curve.")
|
||||
if 'burst_altitude' not in data or data['burst_altitude'] <= launch_alt:
|
||||
raise serializers.ValidationError("burst_altitude must be greater than launch_altitude.")
|
||||
|
||||
# кастомная логика clipping'а
|
||||
if 'ascent_rate' in data:
|
||||
data['ascent_rate'] = rate_clip(data['ascent_rate'])
|
||||
if 'descent_rate' in data:
|
||||
data['descent_rate'] = rate_clip(data['descent_rate'])
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
class PredictionListSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Prediction
|
||||
fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
|
||||
class PredictionDetailSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Prediction
|
||||
fields = ["id", "created_at", "updated_at", "result"]
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,52 +0,0 @@
|
|||
import requests
|
||||
from urllib.parse import urlencode
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from zoneinfo import ZoneInfo
|
||||
from collections import OrderedDict
|
||||
|
||||
class TawhiriClient:
|
||||
BASE_URL = "https://fly.stratonautica.ru/api/v2/"
|
||||
TIMEOUT = 15
|
||||
|
||||
@staticmethod
|
||||
def _convert_value(value: Any) -> Any:
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat().replace("+00:00", "Z")
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def get_prediction(cls, params: dict) -> dict:
|
||||
url = cls.build_url(params)
|
||||
print("🔍 URL:", url)
|
||||
response = requests.get(url, timeout=cls.TIMEOUT)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@classmethod
|
||||
def build_url(cls, params: dict) -> str:
|
||||
query = OrderedDict()
|
||||
|
||||
query["profile"] = params.get("profile")
|
||||
query["launch_datetime"] = cls._convert_value(params.get("launch_datetime"))
|
||||
query["launch_latitude"] = params.get("launch_latitude")
|
||||
query["launch_longitude"] = params.get("launch_longitude")
|
||||
query["launch_altitude"] = params.get("launch_altitude", 0)
|
||||
query["ascent_rate"] = params.get("ascent_rate")
|
||||
query["burst_altitude"] = params.get("burst_altitude")
|
||||
query["descent_rate"] = params.get("descent_rate")
|
||||
query["interpolate"] = str(params.get("interpolate", False)).lower()
|
||||
#query["dataset"] = cls._convert_value(params.get("dataset"))
|
||||
query["format"] = params.get("format", "json")
|
||||
query["pred_type"] = "single" # <-- в конце
|
||||
|
||||
filtered = {k: v for k, v in query.items() if v is not None}
|
||||
return f"{cls.BASE_URL}?{urlencode(filtered)}"
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_prediction(cls, params: dict) -> dict:
|
||||
url = cls.build_url(params)
|
||||
response = requests.get(url, timeout=cls.TIMEOUT)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
from django.urls import path
|
||||
from .views import (PredictionCreateView, PredictionListView, PredictionDeleteView,
|
||||
PredictionHistoryListView,
|
||||
PredictionHistoryDetailView,
|
||||
PredictionHistoryDeleteView,)
|
||||
from .views import PredictionCreateView, PredictionListView, PredictionDeleteView
|
||||
from rest_framework.authtoken.views import obtain_auth_token
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -10,7 +7,4 @@ urlpatterns = [
|
|||
path('predictions', PredictionListView.as_view(), name='get_predictions'),
|
||||
path('predictions/<uuid:pk>', PredictionDeleteView.as_view(), name='delete_prediction'),
|
||||
path('token/', obtain_auth_token),
|
||||
path("history/", PredictionHistoryListView.as_view()),
|
||||
path("history/<uuid:pk>/", PredictionHistoryDetailView.as_view()),
|
||||
path("history/<uuid:pk>/delete/", PredictionHistoryDeleteView.as_view()),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
import base64
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def rate_clip(rate):
|
||||
"""Ограничивает допустимые значения скорости (например, 0.1 ≤ x ≤ 100)"""
|
||||
return min(max(rate, 0.1), 100.0)
|
||||
|
||||
|
||||
def _rfc3339_to_timestamp(value):
|
||||
"""Парсинг RFC 3339 строки в datetime"""
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
|
||||
|
||||
def base64_to_curve(encoded):
|
||||
"""Декодирует base64-encoded curve"""
|
||||
try:
|
||||
decoded = base64.b64decode(encoded).decode('utf-8')
|
||||
return json.loads(decoded)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid curve format: {e}")
|
||||
|
||||
|
||||
def validate_custom_curve(curve):
|
||||
"""Проверяет, что curve имеет ожидаемую структуру (например, список точек)"""
|
||||
try:
|
||||
points = base64_to_curve(curve)
|
||||
return isinstance(points, list) and all(isinstance(p, list) and len(p) == 2 for p in points)
|
||||
except Exception:
|
||||
return False
|
||||
83
api/views.py
83
api/views.py
|
|
@ -1,19 +1,11 @@
|
|||
from rest_framework import status, generics, permissions
|
||||
from rest_framework import status, generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from django.utils import timezone
|
||||
from .models import Prediction, User, UserPrediction
|
||||
from .serializers import PredictionSerializer, PredictionRequestSerializer, PredictionListSerializer, PredictionDetailSerializer
|
||||
from .serializers import PredictionSerializer
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
import requests
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
from rest_framework.permissions import AllowAny
|
||||
from .services.tawhiri import TawhiriClient
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def get_prediction_from_tawhiri(params):
|
||||
base_url = "https://fly.stratonautica.ru/api/v2"
|
||||
|
|
@ -24,33 +16,24 @@ def get_prediction_from_tawhiri(params):
|
|||
else:
|
||||
raise Exception(f"Tawhiri error: {response.status_code} {response.text}")
|
||||
|
||||
|
||||
class PredictionCreateView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
print("DEBUG: request.user =", request.user)
|
||||
print("DEBUG: request.user.id =", request.user.id)
|
||||
user_id = request.data.get('user_id')
|
||||
user = User.objects.get(id=user_id)
|
||||
|
||||
serializer = PredictionRequestSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
validated_data = serializer.validated_data
|
||||
# Передаём остальные параметры (кроме user_id) в Tawhiri
|
||||
tawhiri_params = {k: v for k, v in request.data.items() if k != 'user_id'}
|
||||
|
||||
try:
|
||||
prediction_result = TawhiriClient.get_prediction(validated_data)
|
||||
except requests.RequestException as e:
|
||||
print("❌ Tawhiri error:", str(e), e.response.text if e.response else "no response")
|
||||
return Response({"error": f"Tawhiri error: {str(e)}"}, status=status.HTTP_502_BAD_GATEWAY)
|
||||
prediction_result = get_prediction_from_tawhiri(tawhiri_params)
|
||||
except Exception as e:
|
||||
return Response({"error": str(e)}, status=500)
|
||||
|
||||
prediction = Prediction.objects.create(result=prediction_result)
|
||||
UserPrediction.objects.create(user=request.user, prediction=prediction, created_at=timezone.now())
|
||||
UserPrediction.objects.create(user=user, prediction=prediction)
|
||||
|
||||
return Response({
|
||||
"id": prediction.id,
|
||||
"created_at": prediction.created_at,
|
||||
"result": prediction_result
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
return Response(PredictionSerializer(prediction).data)
|
||||
|
||||
class PredictionListView(APIView):
|
||||
def get(self, request):
|
||||
|
|
@ -76,43 +59,5 @@ class PredictionDeleteView(APIView):
|
|||
except Prediction.DoesNotExist:
|
||||
return Response({"error": "Not found"}, status=404)
|
||||
|
||||
|
||||
|
||||
class PredictionHistoryListView(generics.ListAPIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = PredictionListSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return Prediction.objects.filter(
|
||||
id__in=UserPrediction.objects.filter(user=self.request.user).values_list('prediction_id', flat=True),
|
||||
deleted_at__isnull=True
|
||||
)
|
||||
|
||||
|
||||
class PredictionHistoryDetailView(generics.RetrieveAPIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = PredictionDetailSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return Prediction.objects.filter(
|
||||
id__in=UserPrediction.objects.filter(user=self.request.user).values_list('prediction_id', flat=True),
|
||||
deleted_at__isnull=True
|
||||
)
|
||||
|
||||
|
||||
class PredictionHistoryDeleteView(generics.DestroyAPIView):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return Prediction.objects.filter(
|
||||
id__in=UserPrediction.objects.filter(user=self.request.user).values_list('prediction_id', flat=True),
|
||||
deleted_at__isnull=True
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.deleted_at = timezone.now()
|
||||
instance.save()
|
||||
|
||||
|
||||
#class PredictionCreateView(APIView):
|
||||
#permission_classes = [IsAuthenticated]
|
||||
class PredictionCreateView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
|
|
@ -40,12 +40,10 @@ INSTALLED_APPS = [
|
|||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'drf_spectacular',
|
||||
'corsheaders',
|
||||
'api'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
|
|
@ -55,8 +53,6 @@ MIDDLEWARE = [
|
|||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
ROOT_URLCONF = 'testapi.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
|
|
@ -146,6 +142,5 @@ REST_FRAMEWORK = {
|
|||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
#'rest_framework.permissions.AllowAny',
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue