Compare commits

..

No commits in common. "cc5187c3a1001508bea64d6c5f46a72de9a46507" and "456551cd4ebca048ee42271855e9e76794789775" have entirely different histories.

17 changed files with 24 additions and 282 deletions

Binary file not shown.

View file

@ -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),
),
]

View file

@ -1,6 +1,6 @@
import uuid import uuid
from django.db import models from django.db import models
from django.contrib.auth import get_user_model
class User(models.Model): class User(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
@ -11,10 +11,10 @@ class Prediction(models.Model):
result = models.JSONField() result = models.JSONField()
deleted_at = models.DateTimeField(null=True, blank=True) deleted_at = models.DateTimeField(null=True, blank=True)
class UserPrediction(models.Model): class UserPrediction(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
prediction = models.ForeignKey("Prediction", on_delete=models.CASCADE) prediction = models.ForeignKey(Prediction, on_delete=models.CASCADE)
created_at = models.DateTimeField() created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('user', 'prediction')

View file

@ -1,86 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Prediction 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 PredictionSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Prediction model = Prediction
fields = ['id', 'created_at', 'updated_at', 'result'] 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"]

View file

@ -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()

View file

@ -1,8 +1,5 @@
from django.urls import path from django.urls import path
from .views import (PredictionCreateView, PredictionListView, PredictionDeleteView, from .views import PredictionCreateView, PredictionListView, PredictionDeleteView
PredictionHistoryListView,
PredictionHistoryDetailView,
PredictionHistoryDeleteView,)
from rest_framework.authtoken.views import obtain_auth_token from rest_framework.authtoken.views import obtain_auth_token
urlpatterns = [ urlpatterns = [
@ -10,7 +7,4 @@ urlpatterns = [
path('predictions', PredictionListView.as_view(), name='get_predictions'), path('predictions', PredictionListView.as_view(), name='get_predictions'),
path('predictions/<uuid:pk>', PredictionDeleteView.as_view(), name='delete_prediction'), path('predictions/<uuid:pk>', PredictionDeleteView.as_view(), name='delete_prediction'),
path('token/', obtain_auth_token), 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()),
] ]

View file

@ -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

View file

@ -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.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.utils import timezone from django.utils import timezone
from .models import Prediction, User, UserPrediction from .models import Prediction, User, UserPrediction
from .serializers import PredictionSerializer, PredictionRequestSerializer, PredictionListSerializer, PredictionDetailSerializer from .serializers import PredictionSerializer
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
import requests 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): def get_prediction_from_tawhiri(params):
base_url = "https://fly.stratonautica.ru/api/v2" base_url = "https://fly.stratonautica.ru/api/v2"
@ -23,34 +15,25 @@ def get_prediction_from_tawhiri(params):
return response.json() # получаем результат предсказания return response.json() # получаем результат предсказания
else: else:
raise Exception(f"Tawhiri error: {response.status_code} {response.text}") raise Exception(f"Tawhiri error: {response.status_code} {response.text}")
class PredictionCreateView(APIView): class PredictionCreateView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request): def post(self, request):
print("DEBUG: request.user =", request.user) user_id = request.data.get('user_id')
print("DEBUG: request.user.id =", request.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: try:
prediction_result = TawhiriClient.get_prediction(validated_data) prediction_result = get_prediction_from_tawhiri(tawhiri_params)
except requests.RequestException as e: except Exception as e:
print("❌ Tawhiri error:", str(e), e.response.text if e.response else "no response") return Response({"error": str(e)}, status=500)
return Response({"error": f"Tawhiri error: {str(e)}"}, status=status.HTTP_502_BAD_GATEWAY)
prediction = Prediction.objects.create(result=prediction_result) 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({ return Response(PredictionSerializer(prediction).data)
"id": prediction.id,
"created_at": prediction.created_at,
"result": prediction_result
}, status=status.HTTP_201_CREATED)
class PredictionListView(APIView): class PredictionListView(APIView):
def get(self, request): def get(self, request):
@ -76,43 +59,5 @@ class PredictionDeleteView(APIView):
except Prediction.DoesNotExist: except Prediction.DoesNotExist:
return Response({"error": "Not found"}, status=404) return Response({"error": "Not found"}, status=404)
class PredictionCreateView(APIView):
permission_classes = [IsAuthenticated]
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]

Binary file not shown.

View file

@ -40,12 +40,10 @@ INSTALLED_APPS = [
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'drf_spectacular', 'drf_spectacular',
'corsheaders',
'api' 'api'
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
@ -55,8 +53,6 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
CORS_ALLOW_ALL_ORIGINS = True
ROOT_URLCONF = 'testapi.urls' ROOT_URLCONF = 'testapi.urls'
TEMPLATES = [ TEMPLATES = [
@ -142,10 +138,9 @@ REST_FRAMEWORK = {
# ВАШИ НАСТРОЙКИ # ВАШИ НАСТРОЙКИ
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_AUTHENTICATION_CLASSES': [ 'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.TokenAuthentication',
], ],
'DEFAULT_PERMISSION_CLASSES': [ 'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated', 'rest_framework.permissions.IsAuthenticated',
#'rest_framework.permissions.AllowAny',
] ]
} }