initial commit

This commit is contained in:
straitz 2025-08-15 00:46:18 +09:00
commit c6961c03c3
33 changed files with 1782 additions and 0 deletions

36
.dockerignore Normal file
View file

@ -0,0 +1,36 @@
__pycache__
**/__pycache__
**/__pycache__/*
venv
*.pyc
*.pyo
*.pyd
*.sqlite3
.Python
env
pip-log.txt
pip-delete-this-directory.txt
.tox
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.git
.vscode
.prettierrc
.mypy_cache
.pytest_cache
.hypothesis
Dockerfile
.env
.dockerignore
docker-compose.override.yml
docker-compose.override
docker-compose.yml
nginx
media
static
postgres

60
.gitignore vendored Normal file
View file

@ -0,0 +1,60 @@
# Python
__pycache__/
*/__pycache__
*.py[cod]
*pyc
*.pyo
*.pyd
*.so
*.egg-info/
*.egg
*.log
# Environments
.env
.env.*
venv/
venv*/
ENV/
.envrc
# Django
*.sqlite3
db.sqlite3
media/
staticfiles/
*.pot
*.pyc
# Migrations (если хочешь игнорировать сгенерированные миграции)
# migrations/
# */migrations/
# IDEs
.vscode/
.idea/
*.swp
# OS
.DS_Store
Thumbs.db
# Tests
.coverage
htmlcov/
*.cover
*.py,cover
.cache/
.tox/
# Git
*.orig
# Docker
*.pid
*.pid.lock
docker-compose.override.yml
# Build
dist/
build/

76
Dockerfile Normal file
View file

@ -0,0 +1,76 @@
FROM python:3.13-slim AS builder
RUN mkdir /www
WORKDIR /www
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# RUN apt-get update && apt-get install -y --no-install-recommends \
# libpq-dev \
# gcc \
# g++ \
# libffi-dev \
# libssl-dev \
# libxml2-dev \
# libxslt1-dev \
# zlib1g-dev \
# libjpeg-dev \
# libfreetype6-dev && \
# rm -rf /var/lib/apt/lists/* && \
# apt-get clean && \
# apt-get autoclean
RUN pip install --upgrade pip
COPY requirements.txt /www/
RUN pip install --no-cache-dir -r requirements.txt
COPY requirements-prod.txt /www/
RUN pip install --no-cache-dir -r requirements-prod.txt
# Stage 2: Production stage
FROM python:3.13-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \
libssl-dev \
gettext && \
apt-get clean && \
apt-get autoclean
RUN useradd -m -r wwwuser && \
mkdir /www && \
chown -R wwwuser /www
# Copy the Python dependencies from the builder stage
COPY --from=builder /usr/local/lib/python3.13/site-packages/ /usr/local/lib/python3.13/site-packages/
COPY --from=builder /usr/local/bin/ /usr/local/bin/
# Set the working directory
WORKDIR /www
# Copy application code
COPY --chown=wwwuser:wwwuser . .
RUN chmod +x /www/entrypoint.sh && \
chmod +x /www/manage.py
RUN mkdir -p /static && \
mkdir -p /media && \
chown -R wwwuser:wwwuser /static && \
chown -R wwwuser:wwwuser /media
# Set environment variables to optimize Python
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Switch to non-root user
USER wwwuser
# Expose the application port
EXPOSE 8000
# Run Djangos development server
CMD ["/www/entrypoint.sh"]

61
docker-compose.yml Normal file
View file

@ -0,0 +1,61 @@
services:
db:
image: postgres:14
environment:
POSTGRES_DB: mydb
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypass
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app_network
redis:
image: redis:latest
container_name: redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app_network
web:
build: .
command: daphne -b 0.0.0.0 -p 8000 stratoflights.asgi:application
environment:
DJANGO_SETTINGS_MODULE: stratoflights.settings
ports:
- "8000:8000"
volumes:
- ./static:/static:rw
- ./media:/media:rw
env_file: .env
depends_on:
- db
networks:
- app_network
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
- ./static:/var/www/static
- ./media:/var/www/media
depends_on:
- web
networks:
- app_network
volumes:
redis_data:
postgres_data:
static_volume:
media_volume:
networks:
app_network:
driver: bridge

4
entrypoint.sh Normal file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
python manage.py collectstatic --noinput
python manage.py migrate --noinput
python -m daphne stratoflights.asgi:application -b 0.0.0.0 -p 8000

22
manage.py Normal file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'stratoflights.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

45
nginx/nginx.conf Normal file
View file

@ -0,0 +1,45 @@
events {}
http {
upstream django {
server web:8000;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name localhost;
# Static files
location /static/ {
alias /app/static/;
expires 1y;
access_log off;
add_header Cache-Control "public";
gzip_static on;
}
# Media files
location /media/ {
alias /app/media/;
expires 7d;
access_log off;
add_header Cache-Control "public";
}
# Django app
location / {
proxy_pass http://django;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
}

1
requirements-prod.txt Normal file
View file

@ -0,0 +1 @@
daphne

13
requirements.txt Normal file
View file

@ -0,0 +1,13 @@
Django>=4.0,<5.0
djangorestframework
djangorestframework-simplejwt
psycopg2-binary
drf-spectacular
requests
django-cors-headers
Pillow
python-dotenv
channels>=4.0
channels_redis
celery
websockets

View file

16
stratoflights/asgi.py Normal file
View file

@ -0,0 +1,16 @@
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "stratoflights.settings")
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import stratoflights_api.routing
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
stratoflights_api.routing.websocket_urlpatterns
)
),
})

13
stratoflights/routing.py Normal file
View file

@ -0,0 +1,13 @@
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from django.core.asgi import get_asgi_application
import stratoflights_api.routing # вот тут важно подключить маршруты приложения
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
stratoflights_api.routing.websocket_urlpatterns
)
),
})

204
stratoflights/settings.py Normal file
View file

@ -0,0 +1,204 @@
"""
Django settings for stratoflights project.
Generated by 'django-admin startproject' using Django 4.2.23.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
import os
from dotenv import load_dotenv
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# Environment flag
PRODUCTION = os.getenv('DJANGO_ENV', "") == 'production'
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv(
'SECRET_KEY', 'django-insecure-np(nxnh6mw)v4pa2n2z3pl_5&!2z$jshhak9r3v=y1u9rd*sl!')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', 'True') == 'True'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost').split(',')
# Static files (CSS, JavaScript, Images)
STATIC_URL = os.getenv('STATIC_URL', '/static/')
STATIC_ROOT = os.getenv('STATIC_ROOT', os.path.join(BASE_DIR, 'static'))
# Media files (user uploaded)
MEDIA_URL = os.getenv('MEDIA_URL', '/media/')
MEDIA_ROOT = os.getenv('MEDIA_ROOT', os.path.join(BASE_DIR, 'media')) # Куда сохранять загруженные файлы
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'drf_spectacular',
'corsheaders',
'stratoflights_api.apps.StratoflightsApiConfig',
'channels',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
CORS_ALLOWED_ORIGINS = [
'http://localhost:5173',
'http://127.0.0.1:5173',
]
CORS_EXPOSE_HEADERS = ['Content-Type', 'X-CSRFToken']
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = True
ROOT_URLCONF = 'stratoflights.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'stratoflights.wsgi.application'
ASGI_APPLICATION = 'stratoflights.asgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
if PRODUCTION:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.getenv('DB_NAME'),
'USER': os.getenv('DB_USER'),
'PASSWORD': os.getenv('DB_PASSWORD'),
'HOST': os.getenv('DB_HOST'),
'PORT': os.getenv('DB_PORT'),
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'stratoflights_api.User'
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 100,
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
# 'rest_framework.permissions.AllowAny',
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
}
CSRF_COOKIE_SAMESITE = 'Lax' # temp
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_SAMESITE = 'Lax' # temp
SESSION_COOKIE_SECURE = False
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:5173, http://localhost:8000').split(',')
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("redis", 6379)],
},
},
}

27
stratoflights/urls.py Normal file
View file

@ -0,0 +1,27 @@
"""
URL configuration for stratoflights project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView
from drf_spectacular.views import SpectacularSwaggerView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('stratoflights_api.urls')),
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='docs'),
]

16
stratoflights/wsgi.py Normal file
View file

@ -0,0 +1,16 @@
"""
WSGI config for stratoflights project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'stratoflights.settings')
application = get_wsgi_application()

View file

View file

@ -0,0 +1,8 @@
from django.contrib import admin
from .models import User, Prediction, Satellite, TelemetryPacket
admin.site.register(User)
admin.site.register(Prediction)
admin.site.register(Satellite)
admin.site.register(TelemetryPacket)

View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class StratoflightsApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'stratoflights_api'

View file

@ -0,0 +1,38 @@
import urllib.parse
def build_query_string(data: dict) -> str:
required_keys = [
"profile",
"pred_type",
"launch_datetime",
"launch_latitude",
"launch_longitude",
"launch_altitude",
"ascent_rate",
"burst_altitude",
"descent_rate"
]
# Проверяем, что все ключи на месте
missing_keys = [key for key in required_keys if key not in data]
if missing_keys:
raise ValueError(f"Missing required keys: {', '.join(missing_keys)}")
# Собираем строку запроса
return urllib.parse.urlencode({k: data[k] for k in required_keys})
# Пример:
json_data = {
"profile": "standard_profile",
"pred_type": "single",
"launch_datetime": "2025-03-16T08:47:00Z",
"launch_latitude": "56.6992",
"launch_longitude": "38.8247",
"launch_altitude": "0",
"ascent_rate": "5",
"burst_altitude": "30000",
"descent_rate": "5"
}
query_string = build_query_string(json_data)
print(query_string)

View file

@ -0,0 +1,106 @@
import json
import time
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.contrib.auth import get_user_model
class TelemetryConsumer(AsyncWebsocketConsumer):
group_prefix = None
write_enabled = False
async def connect(self):
User = get_user_model()
from rest_framework.authtoken.models import Token
from .models import Satellite
token_key = self.scope["query_string"].decode().split("token=")[-1]
self.satellite_id = self.scope["url_route"]["kwargs"]["pk"]
self.group_name = f"telemetry_{self.satellite_id}"
user = await self.get_user_from_token(token_key, Token)
if not user:
await self.send(text_data=json.dumps({"error": "Invalid token"}))
await self.close()
return
self.scope["user"] = user
try:
self.satellite = await database_sync_to_async(Satellite.objects.get)(id=self.satellite_id)
except Satellite.DoesNotExist:
await self.send(text_data=json.dumps({"error": "Invalid satellite"}))
await self.close()
return
await self.channel_layer.group_add(self.group_name, self.channel_name)
await self.accept()
async def receive(self, text_data):
from .serializers import TelemetryPacketSerializer
if not self.write_enabled:
await self.send(text_data=json.dumps({"error": "Read-only mode"}))
return
data = json.loads(text_data)
serializer = TelemetryPacketSerializer(data=data)
if not serializer.is_valid():
await self.send(text_data=json.dumps({"error": serializer.errors}))
return
saved_data = await self.save_telemetry(data)
await self.channel_layer.group_send(
self.group_name,
{
"type": "telemetry_message",
"data": {
"id": str(saved_data.id),
"timestamp": saved_data.timestamp,
"lat": saved_data.lat,
"lon": saved_data.lon,
"alt": saved_data.alt,
"payload": saved_data.payload,
"raw_data": saved_data.raw_data,
},
},
)
async def telemetry_message(self, event):
await self.send(text_data=json.dumps(event["data"]))
@database_sync_to_async
def get_user_from_token(self, token_key, Token):
User = get_user_model()
try:
token = Token.objects.select_related("user").get(key=token_key)
return token.user
except Token.DoesNotExist:
return None
@database_sync_to_async
def save_telemetry(self, data):
User = get_user_model()
from .models import TelemetryPacket
packet = TelemetryPacket.objects.create(
satellite=self.satellite,
user=self.scope["user"],
timestamp=data.get("timestamp", int(time.time())),
lat=data.get("lat", 0.0),
lon=data.get("lon", 0.0),
alt=data.get("alt", 0.0),
payload=data.get("payload", {}),
raw_data=data.get("raw_data", {}),
)
return packet
class SatelliteTelemetryConsumer(TelemetryConsumer):
group_prefix = "satellite"
write_enabled = True
class StationTelemetryConsumer(TelemetryConsumer):
group_prefix = "station"
write_enabled = False

View file

@ -0,0 +1,17 @@
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.response import Response
class CustomLimitOffsetPagination(LimitOffsetPagination):
limit_query_param = 'limit'
offset_query_param = 'skip'
max_limit = 100
default_limit = 10
def get_paginated_response(self, data):
return Response({
'total': self.count,
'limit': self.limit,
'skip': self.offset,
'predictions': data
})

View file

@ -0,0 +1,149 @@
# Generated by Django 4.2.23 on 2025-08-11 16:20
from django.conf import settings
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Satellite',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=100)),
('metadata', models.JSONField(blank=True, default=dict)),
('image', models.ImageField(blank=True, null=True, upload_to='satellite_images/')),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='TelemetryPacket',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('timestamp', models.BigIntegerField()),
('lat', models.FloatField(default=0.0)),
('lon', models.FloatField(default=0.0)),
('alt', models.FloatField(default=0.0)),
('payload', models.JSONField(blank=True, default=dict)),
('raw_data', models.JSONField(blank=True, default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('satellite', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='telemetry', to='stratoflights_api.satellite')),
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='SavedRateProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('type', models.CharField(default='ascent', max_length=50)),
('rate_profile_data', models.JSONField(blank=True, default=dict)),
('is_default', models.BooleanField(default=False)),
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'name')},
},
),
migrations.CreateModel(
name='SavedPoint',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('lat', models.FloatField(default=0.0)),
('lon', models.FloatField(default=0.0)),
('alt', models.FloatField(default=0.0)),
('is_default', models.BooleanField(default=False)),
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'name')},
},
),
migrations.CreateModel(
name='PreditctionTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('is_default', models.BooleanField(default=False)),
('description', models.TextField(blank=True, null=True)),
('prediction_mode', models.CharField(default='', max_length=50)),
('model', models.CharField(default='', max_length=50)),
('dataset', models.CharField(default='', max_length=50)),
('flight_parameters', models.JSONField(blank=True, default=dict)),
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'name')},
},
),
migrations.CreateModel(
name='Prediction',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('request', models.JSONField(default=dict)),
('result', models.JSONField(default=dict)),
('rate_profile', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='predictions', to='stratoflights_api.savedrateprofile')),
('satellite', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='predictions', to='stratoflights_api.satellite')),
('start_point', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='predictions', to='stratoflights_api.savedpoint')),
('template', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='predictions', to='stratoflights_api.preditctiontemplate')),
('user', models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, related_name='predictions', to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='user',
name='satellites',
field=models.ManyToManyField(related_name='users', to='stratoflights_api.satellite'),
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'),
),
]

View file

106
stratoflights_api/models.py Normal file
View file

@ -0,0 +1,106 @@
import uuid
from django.db import models
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser, AnonymousUser
class User(AbstractUser):
satellites = models.ManyToManyField("Satellite", related_name="users")
class Prediction(models.Model):
user = models.ForeignKey(get_user_model(
), on_delete=models.CASCADE, default=0, related_name='predictions')
satellite = models.ForeignKey(
'Satellite', on_delete=models.CASCADE, related_name='predictions', null=True)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
request = models.JSONField(default=dict)
result = models.JSONField(default=dict)
start_point = models.ForeignKey(
'SavedPoint', on_delete=models.SET_NULL, related_name='predictions', null=True)
template = models.ForeignKey(
'PreditctionTemplate', on_delete=models.SET_NULL, related_name='predictions', null=True)
rate_profile = models.ForeignKey(
'SavedRateProfile', on_delete=models.SET_NULL, related_name='predictions', null=True)
class Satellite(models.Model):
user = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, default=0)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100)
metadata = models.JSONField(blank=True, default=dict)
image = models.ImageField(
upload_to='satellite_images/', blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
class TelemetryPacket(models.Model):
user = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, default=0)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
satellite = models.ForeignKey(
Satellite, on_delete=models.CASCADE, related_name="telemetry")
timestamp = models.BigIntegerField() # unix time
lat = models.FloatField(default=0.0)
lon = models.FloatField(default=0.0)
alt = models.FloatField(default=0.0)
payload = models.JSONField(blank=True, default=dict)
raw_data = models.JSONField(blank=True, default=dict)
created_at = models.DateTimeField(auto_now_add=True)
class PreditctionTemplate(models.Model):
user = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, default=0)
name = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
is_default = models.BooleanField(default=False)
description = models.TextField(blank=True, null=True)
prediction_mode = models.CharField(
max_length=50,
default="") # TODO: add choices for prediction modes
model = models.CharField(
max_length=50,
default="") # TODO: add choices for models
dataset = models.CharField(
max_length=50,
default="")
flight_parameters = models.JSONField(blank=True, default=dict)
class Meta:
unique_together = ('user', 'name')
class SavedPoint(models.Model):
user = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, default=0)
name = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
lat = models.FloatField(default=0.0)
lon = models.FloatField(default=0.0)
alt = models.FloatField(default=0.0)
is_default = models.BooleanField(default=False)
class Meta:
unique_together = ('user', 'name')
class SavedRateProfile(models.Model):
user = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, default=0)
name = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
type = models.CharField(
max_length=50, default="ascent") # TODO: add choices for rate profile types
rate_profile_data = models.JSONField(blank=True, default=dict)
is_default = models.BooleanField(default=False)
class Meta:
unique_together = ('user', 'name')

View file

@ -0,0 +1,16 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS
class ReadOnlyOrAuthenticated(BasePermission):
def has_permission(self, request, view):
return (
request.method in SAFE_METHODS or
request.user and request.user.is_authenticated
)
class IsOwner(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.user == request.user
def has_permission(self, request, view):
return request.user and request.user.is_authenticated

View file

@ -0,0 +1,7 @@
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"^api/ws/satellite/(?P<pk>[0-9a-f-]+)/telemetry/$", consumers.SatelliteTelemetryConsumer.as_asgi()),
re_path(r"^api/ws/station/(?P<pk>[0-9a-f-]+)/telemetry/$", consumers.StationTelemetryConsumer.as_asgi()),
]

View file

@ -0,0 +1,185 @@
from rest_framework import serializers
from .models import Prediction, SavedPoint, SavedRateProfile, PreditctionTemplate
from datetime import datetime
from django.contrib.auth.password_validation import validate_password
from django.core.validators import validate_email
from django.core.exceptions import ValidationError as DjangoValidationError
from django.contrib.auth import get_user_model
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']
User = get_user_model()
PROFILE_STANDARD = "standard_profile"
PROFILE_FLOAT = "float_profile"
PROFILE_REVERSE = "reverse_profile"
PROFILE_CUSTOM = "custom_profile"
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)
start_point = serializers.PrimaryKeyRelatedField(
queryset=SavedPoint.objects.all(), required=False, allow_null=True
)
rate_profile = serializers.PrimaryKeyRelatedField(
queryset=SavedRateProfile.objects.all(), required=False, allow_null=True
)
template = serializers.PrimaryKeyRelatedField(
queryset=PreditctionTemplate.objects.all(), required=False, allow_null=True
)
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
def create(self, validated_data):
if 'ascent_curve' in validated_data:
validated_data['ascent_curve'] = base64_to_curve(validated_data['ascent_curve'])
if 'descent_curve' in validated_data:
validated_data['descent_curve'] = base64_to_curve(validated_data['descent_curve'])
prediction = Prediction(
user=validated_data.get('user'),
request=validated_data.get('request', {}),
result=validated_data.get('result', {}),
start_point=validated_data.get('start_point'),
template=validated_data.get('template'),
rate_profile=validated_data.get('rate_profile')
)
prediction.save()
return prediction
class PredictionListSerializer(serializers.ModelSerializer):
class Meta:
model = Prediction
fields = ["id", "created_at", "updated_at", "start_point", "template", "rate_profile"]
class PredictionDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Prediction
fields = ["id", "created_at", "updated_at", "result", "start_point", "template", "rate_profile"]
from rest_framework import serializers
from .models import TelemetryPacket
class TelemetryPacketSerializer(serializers.ModelSerializer):
class Meta:
model = TelemetryPacket
fields = ['id', 'timestamp', 'lat', 'lon', 'alt', 'payload']
read_only_fields = ['id']
class SavedPointSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
class Meta:
model = SavedPoint
fields = ['user', 'id', 'name', 'lat', 'lon', 'alt']
read_only_fields = ['id']
validators = [
serializers.UniqueTogetherValidator(
queryset=SavedPoint.objects.all(),
fields=['user', 'name'],
message="A saved point with this name already exists for the user."
)
]
class SavedRateProfileSerializer(serializers.ModelSerializer):
class Meta:
model = SavedRateProfile
fields = ['id', 'name', 'type', 'rate_profile_data']
read_only_fields = ['id']
class PreditctionTemplateSerializer(serializers.ModelSerializer):
class Meta:
model = PreditctionTemplate
fields = ['id', 'name', 'is_default', 'description', 'prediction_mode', 'model', 'dataset', 'flight_parameters']
read_only_fields = ['id']
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name']
extra_kwargs = {
'username': {'read_only': True}
}
def validate_email(self, value):
try:
validate_email(value)
except DjangoValidationError:
raise serializers.ValidationError("Invalid email format")
return value
class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)
def validate_new_password(self, value):
validate_password(value)
return value
class DeleteAccountSerializer(serializers.Serializer):
password = serializers.CharField(required=True)

View file

@ -0,0 +1,52 @@
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

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

48
stratoflights_api/urls.py Normal file
View file

@ -0,0 +1,48 @@
from django.urls import path
from rest_framework.routers import DefaultRouter
from rest_framework.authtoken.views import obtain_auth_token
from .views import (
PredictionViewSet,
SavedPointViewset,
PreditctionTemplateViewset,
TelemetryListCreateView,
get_csrf,
login_view,
logout_view,
SessionView,
WhoAmIView,
UserProfileView,
ChangePasswordView,
TokenManagementView,
DeleteUserDataView,
DeleteAccountView
)
router = DefaultRouter()
router.register(r'predictions', PredictionViewSet, basename='predictions')
router.register(r'saved-points', SavedPointViewset, basename='saved-points')
router.register(r'saved-templates', PreditctionTemplateViewset, basename='saved-templates')
urlpatterns = [
path("csrf/", get_csrf, name='api-csrf'),
path('token', obtain_auth_token, name = 'get_token'),
path("login/", login_view, name='api-login'),
path("logout/", logout_view, name='api-logout'),
path("session/", SessionView.as_view(), name='api-session'),
path("whoami/", WhoAmIView.as_view(), name='api-whoami'),
path("<uuid:pk>/telemetry/", TelemetryListCreateView.as_view(), name="create_telemetry"),
path('csrf/', get_csrf, name='api-csrf'),
path('login/', login_view, name='api-login'),
path('logout/', logout_view, name='api-logout'),
path('session/', SessionView.as_view(), name='api-session'),
path('whoami/', WhoAmIView.as_view(), name='api-whoami'),
path("profile/", UserProfileView.as_view(), name='api-profile'),
path("profile/change-password/", ChangePasswordView.as_view(), name='api-change-password'),
path("profile/token/", TokenManagementView.as_view(), name='api-token'),
path("profile/delete-data/", DeleteUserDataView.as_view(), name='api-delete-data'),
path("profile/delete-account/", DeleteAccountView.as_view(), name='api-delete-account'),
]
urlpatterns += router.urls

View file

@ -0,0 +1,31 @@
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

354
stratoflights_api/views.py Normal file
View file

@ -0,0 +1,354 @@
import requests
import time
import json
from rest_framework import status, generics, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet, ViewSet, GenericViewSet
from rest_framework.exceptions import APIException
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.authentication import SessionAuthentication, BasicAuthentication, TokenAuthentication
from rest_framework.decorators import api_view, permission_classes, authentication_classes, action
from rest_framework.authtoken.models import Token
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.http import JsonResponse
from django.contrib.auth import authenticate, login, logout, get_user_model
from django.middleware.csrf import get_token
from django.core.exceptions import ValidationError
from django.utils.dateparse import parse_datetime
from .models import Prediction, User, Satellite, SavedPoint, SavedRateProfile, PreditctionTemplate, TelemetryPacket
from .serializers import PredictionSerializer, TelemetryPacketSerializer, PredictionRequestSerializer, PredictionListSerializer, PredictionDetailSerializer, SavedPointSerializer, SavedRateProfileSerializer, PreditctionTemplateSerializer, UserSerializer, ChangePasswordSerializer, DeleteAccountSerializer
from .services.tawhiri import TawhiriClient
from drf_spectacular.utils import extend_schema
from .permissions import ReadOnlyOrAuthenticated, IsOwner
from .custom_pagination import CustomLimitOffsetPagination
from datetime import datetime
User = get_user_model()
def get_prediction_from_tawhiri(params):
base_url = "https://fly.stratonautica.ru/api/v2"
response = requests.get(base_url, params=params)
if response.status_code == 200:
return response.json() # получаем результат предсказания
else:
raise Exception(
f"Tawhiri error: {response.status_code} {response.text}")
class PredictionViewSet(GenericViewSet):
permission_classes = [IsAuthenticated]
pagination_class = CustomLimitOffsetPagination
def list(self, request):
queryset = Prediction.objects.filter(user=request.user)
return Response(PredictionSerializer(queryset, many=True).data)
def create(self, request):
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
try:
prediction_result = TawhiriClient.get_prediction(validated_data)
except requests.RequestException as e:
return Response({"error": f"Tawhiri error: {str(e)}"}, status=status.HTTP_502_BAD_GATEWAY)
# prediction = Prediction.objects.create(
# result=prediction_result, user=request.user, request=request.data, validated_data=validated_data)
prediction = serializer.save(
user=request.user,
result=prediction_result,
request=request.data
)
return Response({
"id": prediction.id,
"created_at": prediction.created_at,
"result": prediction_result
}, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['get'])
def list_user(self, request):
user = request.user
satellite_id = request.query_params.get('satellite_id')
created_from = request.query_params.get('created_from')
created_till = request.query_params.get('created_till')
filters = {
'user': user,
}
if created_from:
filters['created_at__gte'] = parse_datetime(created_from)
if created_till:
filters['created_at__lte'] = parse_datetime(created_till)
if satellite_id:
if not user.satellites.filter(id=satellite_id).exists():
return Response({'detail': 'Access denied'}, status=403)
filters['satellite_id'] = satellite_id
queryset = Prediction.objects.filter(**filters)
queryset = self.filter_queryset(queryset)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = PredictionSerializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = PredictionSerializer(queryset, many=True)
return Response(serializer.data)
@action(detail=False, methods=["get"])
def history(self, request):
queryset = Prediction.objects.filter(user=request.user)
return Response(PredictionListSerializer(queryset, many=True).data)
@action(detail=True, methods=["get"])
def detail(self, request, pk=None):
prediction = Prediction.objects.filter(
user=request.user, pk=pk).first()
if not prediction:
return Response({'detail': 'Not found'}, status=404)
return Response(PredictionDetailSerializer(prediction).data)
@action(detail=True, methods=["delete"])
def delete(self, request, pk=None):
prediction = Prediction.objects.filter(
user=request.user, pk=pk).first()
if not prediction:
return Response({'detail': 'Not found'}, status=404)
prediction.delete()
return Response(status=204)
class TelemetryListCreateView(generics.ListCreateAPIView):
serializer_class = TelemetryPacketSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
qs = TelemetryPacket.objects.filter(satellite_id=self.kwargs["pk"])
from_ts = self.request.query_params.get("from")
till_ts = self.request.query_params.get("till")
if from_ts:
qs = qs.filter(timestamp__gte=int(from_ts))
if till_ts:
qs = qs.filter(timestamp__lte=int(till_ts))
return qs.order_by("-timestamp")
def post(self, request, pk):
serializer = TelemetryPacketSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
validated_data = serializer.validated_data
TelemetryPacket.objects.create(timestamp=time.time(),
satellite=Satellite.objects.get(id=pk),
lat=validated_data["lat"],
lon=validated_data["lon"],
alt=validated_data["alt"],
payload=validated_data['payload'],
)
return Response(serializer.errors, status=status.HTTP_201_CREATED)
class SessionView(APIView):
permission_classes = [IsAuthenticated]
@staticmethod
def get(request, format=None):
return JsonResponse({'isAuthenticated': True})
class WhoAmIView(APIView):
permission_classes = [IsAuthenticated]
@staticmethod
def get(request, format=None):
return JsonResponse({'username': request.user.username})
@extend_schema(methods=["GET"], description="Get CSRF token")
@csrf_exempt
@api_view(["GET"])
@permission_classes([AllowAny])
def get_csrf(request):
response = JsonResponse({'detail': 'CSRF cookie set'})
response['X-CSRFToken'] = get_token(request)
return response
@extend_schema(methods=["POST"], description="Login user")
@csrf_exempt
@api_view(["POST"])
@authentication_classes([BasicAuthentication])
@permission_classes([AllowAny])
def login_view(request):
data = json.loads(request.body)
username = data.get('username')
password = data.get('password')
if username is None or password is None:
return JsonResponse({'detail': 'Please provide username and password.'}, status=400)
user = authenticate(username=username, password=password)
if user is None:
return JsonResponse({'detail': 'Invalid credentials.'}, status=400)
login(request, user)
return JsonResponse({'detail': 'Successfully logged in.'})
@extend_schema(methods=["POST"], description="Logout user")
@api_view(["POST"])
@permission_classes([AllowAny])
def logout_view(request):
if not request.user.is_authenticated:
return JsonResponse({'detail': 'You\'re not logged in.'}, status=400)
logout(request)
return JsonResponse({'detail': 'Successfully logged out.'})
class SavedPointViewset(ModelViewSet):
permission_classes = [IsOwner]
serializer_class = SavedPointSerializer
pagination_class = None
def get_queryset(self):
return SavedPoint.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class PreditctionTemplateViewset(ModelViewSet):
permission_classes = [IsOwner]
serializer_class = PreditctionTemplateSerializer
pagination_class = None
def get_queryset(self):
return PreditctionTemplate.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class UserProfileView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
def patch(self, request):
user = request.user
serializer = UserSerializer(user, data=request.data, partial=True)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.save()
return Response(serializer.data)
class ChangePasswordView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
serializer = ChangePasswordSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
if not user.check_password(serializer.validated_data['old_password']):
return Response({'detail': 'Old password is incorrect'},
status=status.HTTP_400_BAD_REQUEST)
user.set_password(serializer.validated_data['new_password'])
user.save()
return Response({'detail': 'Password changed successfully'})
class DeleteAccountView(APIView):
permission_classes = [IsAuthenticated]
def delete(self, request):
user = request.user
serializer = DeleteAccountSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
if not user.check_password(serializer.validated_data['password']):
return Response({'detail': 'Incorrect password'},
status=status.HTTP_400_BAD_REQUEST)
Prediction.objects.filter(user=user).delete()
SavedPoint.objects.filter(user=user).delete()
PreditctionTemplate.objects.filter(user=user).delete()
user.delete()
return Response({'detail': 'Account deleted successfully'})
class DeleteUserDataView(APIView):
permission_classes = [IsAuthenticated]
def delete(self, request):
user = request.user
Prediction.objects.filter(user=user).delete()
SavedPoint.objects.filter(user=user).delete()
PreditctionTemplate.objects.filter(user=user).delete()
return Response({'detail': 'All user data deleted successfully'})
class TokenManagementView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
token, created = Token.objects.get_or_create(user=request.user)
return Response({"token": token.key})
def post(self, request):
Token.objects.filter(user=request.user).delete()
token = Token.objects.create(user=request.user)
return Response({"token": token.key})
# class PredictionCreateView(APIView):
# permission_classes = [IsAuthenticated]
# class TelemetryPacket(models.Model):
# id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# satellite = models.ForeignKey(Satellite, on_delete=models.CASCADE, related_name="telemetry")
# timestamp = models.BigIntegerField() # unix time
# lat = models.FloatField()
# lon = models.FloatField()
# alt = models.FloatField()
# payload = models.JSONField(null=True, blank=True)
# created_at = models.DateTimeField(auto_now_add=True)
# fields = ['id', 'timestamp', 'lat', 'lon', 'alt', 'payload']

View file

@ -0,0 +1,62 @@
import asyncio
import json
import websockets
import uuid
import time
BASE_URL = "ws://localhost:8000/api/ws"
TOKEN = "ae397229f9ca50cd6320cb0416671ecc780671ac"
PK = "cf1e36ff-c5ce-4852-8b90-046737974b97"
async def satellite_mode():
"""
Клиент для отправки данных телеметрии.
"""
url = f"{BASE_URL}/satellite/{PK}/telemetry/?token={TOKEN}"
async with websockets.connect(url) as ws:
print(f"[satellite] Connected to {url}")
while True:
telemetry_data = {
"timestamp": int(time.time()),
"lat": 55.75,
"lon": 37.61,
"alt": 200.0,
"payload": {"temp": 22.5, "status": "OK"},
"raw_data": {"sensor": "gps", "accuracy": "high"}
}
await ws.send(json.dumps(telemetry_data))
print(f"[satellite] Sent: {telemetry_data}")
try:
response = await asyncio.wait_for(ws.recv(), timeout=2)
print(f"[satellite] Received: {response}")
except asyncio.TimeoutError:
print("[satellite] No response yet")
await asyncio.sleep(5)
async def station_mode():
"""
Клиент для приёма данных телеметрии.
"""
url = f"{BASE_URL}/station/{PK}/telemetry/?token={TOKEN}"
async with websockets.connect(url) as ws:
print(f"[station] Connected to {url}")
while True:
try:
message = await ws.recv()
print(f"[station] Received: {message}")
except websockets.ConnectionClosed:
print("[station] Connection closed")
break
if __name__ == "__main__":
mode = input("Enter mode (satellite/station): ").strip().lower()
if mode == "satellite":
asyncio.run(satellite_mode())
else:
asyncio.run(station_mode())