rework main models

This commit is contained in:
2025-10-30 09:19:55 +03:00
parent 178854c6ba
commit 94df5171db
25 changed files with 744 additions and 190 deletions

View File

@@ -18,34 +18,26 @@ RUN apt-get update && apt-get install -y \
postgresql-client \
build-essential \
libpq-dev \
gcc \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies for GDAL
RUN pip install --upgrade pip && \
pip install --no-cache-dir GDAL==$(gdal-config --version)
# Set work directory
WORKDIR /app
# Copy project requirements
# Copy project files
COPY pyproject.toml uv.lock ./
# Install uv package manager
RUN pip install --upgrade pip && pip install uv
# Install uv and dependencies
RUN pip install --no-cache-dir uv && \
uv sync --frozen --no-dev
# Install dependencies using uv
RUN uv pip install --system --no-cache-dir -r uv.lock
# Copy project
# Copy project code (после установки зависимостей для лучшего кэширования)
COPY . .
# Collect static files
RUN python manage.py collectstatic --noinput
RUN uv run manage.py collectstatic --noinput
# Expose port
EXPOSE 8000
# Run gunicorn server
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "dbapp.wsgi:application"]
CMD [".venv/bin/gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "dbapp.wsgi:application"]

View File

@@ -72,6 +72,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"debug_toolbar.middleware.DebugToolbarMiddleware", #Добавил
'django.middleware.locale.LocaleMiddleware', #Добавил
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', #Добавил

Binary file not shown.

View File

@@ -6,6 +6,7 @@ from .models import (
Standard,
SigmaParMark,
SigmaParameter,
SourceType,
Parameter,
Satellite,
Mirror,
@@ -128,6 +129,12 @@ class ModulationAdmin(admin.ModelAdmin):
search_fields = ("name",)
ordering = ("name",)
@admin.register(SourceType)
class ModulationAdmin(admin.ModelAdmin):
list_display = ("name",)
search_fields = ("name",)
ordering = ("name",)
@admin.register(Standard)
class StandardAdmin(admin.ModelAdmin):
@@ -209,23 +216,25 @@ class ParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
class SigmaParameterAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
list_display = (
"id_satellite",
"status",
# "status",
"frequency",
"transfer_frequency",
"freq_range",
"power",
# "power",
"polarization",
"modulation",
"bod_velocity",
"snr",
"standard",
# "standard",
"parameter",
"packets",
# "packets",
"datetime_begin",
"datetime_end",
)
readonly_fields = (
"datetime_begin",
"datetime_end",
"transfer_frequency"
)
list_display_links = ("id_satellite",)
list_filter = (
@@ -401,7 +410,7 @@ class ObjectAdmin(admin.ModelAdmin):
)
search_fields = (
"name",
# "id_geo",
"id_geo__coords",
# "id_satellite__name",
# "id_vch_load__frequency",
)
@@ -413,6 +422,7 @@ class ObjectAdmin(admin.ModelAdmin):
"id_vch_load__modulation",
"id_vch_load__id_satellite",
"id_geo",
"id_source_type"
)
autocomplete_fields = ("id_geo",)
raw_id_fields = ("id_vch_load",)
@@ -422,11 +432,13 @@ class ObjectAdmin(admin.ModelAdmin):
def sat_name(self, obj):
return obj.id_vch_load.id_satellite
sat_name.short_description = "Спутник"
sat_name.admin_order_field = "id_vch_load__id_satellite__name"
def freq(self, obj):
par = obj.id_vch_load
return par.frequency
freq.short_description = "Частота, МГц"
freq.admin_order_field = "id_vch_load__frequency"
def distance_geo_kup(self, obj):
par = obj.id_geo.distance_coords_kup
@@ -458,6 +470,7 @@ class ObjectAdmin(admin.ModelAdmin):
par = obj.id_vch_load
return par.freq_range
freq_range.short_description = "Полоса, МГц"
freq_range.admin_order_field = "id_vch_load__freq_range"
def bod_velocity(self, obj):
par = obj.id_vch_load
@@ -482,6 +495,7 @@ class ObjectAdmin(admin.ModelAdmin):
lat = f"{latitude}N" if latitude > 0 else f"{abs(latitude)}S"
return f"{lat} {lon}"
geo_coords.short_description = "Координаты геолокации"
geo_coords.admin_order_filed = "id_geo__coords"
def kupsat_coords(self, obj):
obj = obj.id_geo

View File

@@ -1,5 +1,14 @@
from django import forms
from .models import Satellite
from .models import Satellite, Polarization
class UploadFileForm(forms.Form):
file = forms.FileField(
label="Выберите файл",
widget=forms.FileInput(attrs={
'class': 'form-file-input'
})
)
class LoadExcelData(forms.Form):
file = forms.FileField(
@@ -33,7 +42,7 @@ class LoadCsvData(forms.Form):
})
)
class UploadFileForm(forms.Form):
class UploadVchLoad(UploadFileForm):
sat_choice = forms.ModelChoiceField(
queryset=Satellite.objects.all(),
label="Выберите спутник",
@@ -41,12 +50,7 @@ class UploadFileForm(forms.Form):
'class': 'form-select'
})
)
file = forms.FileField(
label="Выберите текстовый файл",
widget=forms.FileInput(attrs={
'class': 'form-file-input'
})
)
class VchLinkForm(forms.Form):
sat_choice = forms.ModelChoiceField(
@@ -76,3 +80,27 @@ class VchLinkForm(forms.Form):
'placeholder': 'Введите второе число'
})
)
class NewEventForm(forms.Form):
# sat_choice = forms.ModelChoiceField(
# queryset=Satellite.objects.all(),
# label="Выберите спутник",
# widget=forms.Select(attrs={
# 'class': 'form-select'
# })
# )
# pol_choice = forms.ModelChoiceField(
# queryset=Polarization.objects.all(),
# label="Выберите поляризацию",
# widget=forms.Select(attrs={
# 'class': 'form-select'
# })
# )
file = forms.FileField(
label="Выберите файл",
widget=forms.FileInput(attrs={
'class': 'form-control',
'accept': '.xlsx,.xls'
})
)

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.2.7 on 2025-10-27 13:10
import django.db.models.deletion
import django.db.models.expressions
import mainapp.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0017_alter_sigmaparameter_parameter'),
]
operations = [
migrations.AddField(
model_name='sigmaparameter',
name='polarization',
field=models.ForeignKey(blank=True, default=mainapp.models.get_default_polarization, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='polarizations_sigma', to='mainapp.polarization', verbose_name='Поляризация'),
),
migrations.AddField(
model_name='sigmaparameter',
name='transfer',
field=models.FloatField(choices=[(-1.0, '-'), (9750.0, '9750 МГц'), (10750.0, '10750 МГц')], default=-1.0, verbose_name='Перенос по частоте'),
),
migrations.AddField(
model_name='sigmaparameter',
name='transfer_frequency',
field=models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.expressions.CombinedExpression(models.F('frequency'), '+', models.F('transfer')), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Частота в Ku, МГц'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-28 05:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0018_sigmaparameter_polarization_sigmaparameter_transfer_and_more'),
]
operations = [
migrations.AlterField(
model_name='satellite',
name='name',
field=models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Имя спутника'),
),
]

View File

@@ -0,0 +1,30 @@
# Generated by Django 5.2.7 on 2025-10-29 14:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mainapp', '0019_alter_satellite_name'),
]
operations = [
migrations.CreateModel(
name='SourceType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, unique=True, verbose_name='Тип источника')),
],
options={
'verbose_name': 'Тип источника',
'verbose_name_plural': 'Типы источников',
},
),
migrations.AddField(
model_name='objitem',
name='id_source_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='objitems', to='mainapp.sourcetype', verbose_name='Тип источника'),
),
]

View File

@@ -2,6 +2,7 @@ from django.db import models
from django.contrib.auth.models import User
from django.contrib.gis.db import models as gis
from django.contrib.gis.db.models import functions
from django.db.models import F, ExpressionWrapper
def get_default_polarization():
obj, created = Polarization.objects.get_or_create(
@@ -96,7 +97,7 @@ class Standard(models.Model):
class Satellite(models.Model):
name = models.CharField(max_length=30, unique=True, verbose_name="Имя спутника", db_index=True)
name = models.CharField(max_length=100, unique=True, verbose_name="Имя спутника", db_index=True)
norad = models.IntegerField(blank=True, null=True, verbose_name="NORAD ID")
def __str__(self):
@@ -107,6 +108,40 @@ class Satellite(models.Model):
verbose_name_plural = "Спутники"
class ObjItem(models.Model):
name = models.CharField(null=True, blank=True, max_length=100, verbose_name="Имя объекта", db_index=True)
# id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="objitems", verbose_name="Спутник")
# id_vch_load = models.ForeignKey(Parameter, on_delete=models.CASCADE, related_name="objitems", verbose_name="ВЧ загрузка")
# id_geo = models.ForeignKey(Geo, on_delete=models.CASCADE, related_name="objitems", verbose_name="Геоданные")
id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Пользователь", null=True, blank=True)
# id_source_type = models.ForeignKey(SourceType, on_delete=models.SET_NULL, related_name="objitems", verbose_name='Тип источника', null=True, blank=True)
def __str__(self):
return f"Объект {self.name}"
class Meta:
verbose_name = "Объект"
verbose_name_plural = "Объекты"
# constraints = [
# models.UniqueConstraint(
# fields=['id_vch_load', 'id_geo'],
# name='unique_objitem_combination'
# )
# ]
class SourceType(models.Model):
name = models.CharField(max_length=50, unique=True, verbose_name="Тип источника")
objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="objitems", null=True)
def __str__(self):
return self.name
class Meta:
verbose_name = "Тип источника"
verbose_name_plural = 'Типы источников'
class Parameter(models.Model):
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="parameters", verbose_name="Спутник", null=True)
polarization = models.ForeignKey(
@@ -123,6 +158,7 @@ class Parameter(models.Model):
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="standards", null=True, blank=True, verbose_name="Стандарт"
)
id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="parameter_added", verbose_name="Пользователь", null=True, blank=True)
objitem = models.ForeignKey(ObjItem, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Источник",null=True, blank=True)
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, on_delete=models.SET_NULL, related_name="sigma_parameter", verbose_name="ВЧ с sigma", null=True, blank=True)
# id_sigma_parameter = models.ManyToManyField(SigmaParameter, verbose_name="ВЧ с sigma", null=True, blank=True)
@@ -151,12 +187,35 @@ class Parameter(models.Model):
class SigmaParameter(models.Model):
TRANSFERS = [
(-1.0, "-"),
(9750.0, "9750 МГц"),
(10750.0, "10750 МГц")
]
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="sigmapar_sat", verbose_name="Спутник")
transfer = models.FloatField(
choices=TRANSFERS,
default=-1.0,
verbose_name="Перенос по частоте"
)
status = models.CharField(max_length=20, blank=True, null=True, verbose_name="Статус")
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц", db_index=True)
transfer_frequency = models.GeneratedField(
expression=ExpressionWrapper(
F('frequency') + F('transfer'),
output_field=models.FloatField()
),
output_field=models.FloatField(),
db_persist=True,
null=True, blank=True, verbose_name="Частота в Ku, МГц"
)
freq_range = models.FloatField(default=0, null=True, blank=True, verbose_name="Полоса частот, МГц")
power = models.FloatField(default=0, null=True, blank=True, verbose_name="Мощность, дБм")
bod_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
polarization = models.ForeignKey(
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="polarizations_sigma", null=True, blank=True, verbose_name="Поляризация"
)
modulation = models.ForeignKey(
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="modulations_sigma", null=True, blank=True, verbose_name="Модуляция"
)
@@ -213,6 +272,7 @@ class Geo(models.Model):
db_persist=True,
null=True, blank=True, verbose_name="Расстояние между купсатом и оперативным отделом, км"
)
objitem = models.OneToOneField(ObjItem, on_delete=models.SET_NULL, verbose_name="Гео", related_name="objitems", null=True)
def __str__(self):
longitude = self.coords.coords[0]
@@ -234,23 +294,3 @@ class Geo(models.Model):
)
]
class ObjItem(models.Model):
name = models.CharField(null=True, blank=True, max_length=100, verbose_name="Имя объекта", db_index=True)
# id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="objitems", verbose_name="Спутник")
id_vch_load = models.ForeignKey(Parameter, on_delete=models.CASCADE, related_name="objitems", verbose_name="ВЧ загрузка")
id_geo = models.ForeignKey(Geo, on_delete=models.CASCADE, related_name="objitems", verbose_name="Геоданные")
id_user_add = models.ForeignKey(CustomUser, on_delete=models.SET_NULL, related_name="objitems", verbose_name="Пользователь", null=True, blank=True)
def __str__(self):
return f"Объект {self.name}"
class Meta:
verbose_name = "Объект"
verbose_name_plural = "Объекты"
constraints = [
models.UniqueConstraint(
fields=['id_vch_load', 'id_geo'],
name='unique_objitem_combination'
)
]

View File

@@ -171,6 +171,26 @@
</div>
</div>
</div>
<!-- New Event Card -->
<div class="col-lg-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div class="bg-success bg-opacity-10 rounded-circle p-2 me-3">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-plus-circle text-success" viewBox="0 0 16 16">
<path d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0M4.5 7.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5M7.5 4.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 0 1h-1a.5.5 0 0 1-.5-.5m1 3a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 1 .5-.5"/>
</svg>
</div>
<h3 class="card-title mb-0">Формирование таблицы для Кубсатов</h3>
</div>
<p class="card-text">Добавьте новое событие с помощью выбора спутника и загрузки файла данных.</p>
<a href="{% url 'kubsat_excel' %}" class="btn btn-success">
Добавить событие
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -31,13 +31,13 @@
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div>
<div class="mb-3">
{% comment %} <div class="mb-3">
<label for="{{ form.ku_range.id_for_label }}" class="form-label">Выберите перенос по частоте(МГц):</label>
{{ form.ku_range }}
{% if form.ku_range.errors %}
<div class="text-danger mt-1">{{ form.ku_range.errors }}</div>
{% endif %}
</div>
</div> {% endcomment %}
<div class="mb-3">
<label for="{{ form.value1.id_for_label }}" class="form-label">Разброс по частоте(в %)</label>
{{ form.value1 }}

View File

@@ -0,0 +1,52 @@
{% extends 'mainapp/base.html' %}
{% block title %}Новое событие{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm border-0">
<div class="card-header bg-success text-white">
<h2 class="mb-0">Формирование таблицы Кубсат</h2>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% comment%}
<div class="mb-4">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">{{ form.sat_choice.label }}</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div>{% endcomment %}
{% comment %} <div class="mb-4">
<label for="{{ form.pol_choice.id_for_label }}" class="form-label">{{ form.pol_choice.label }}</label>
{{ form.pol_choice }}
{% if form.pol_choice.errors %}
<div class="text-danger mt-1">{{ form.pol_choice.errors }}</div>
{% endif %}
</div> {% endcomment %}
<div class="mb-4">
<label for="{{ form.file.id_for_label }}" class="form-label">{{ form.file.label }}</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">Выберите файл для загрузки</div>
</div>
<div class="d-flex justify-content-between">
<a href="{% url 'home' %}" class="btn btn-secondary">Назад</a>
<button type="submit" class="btn btn-success">Выполнить</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,53 @@
{% extends 'mainapp/base.html' %}
{% block title %}Загрузка данных транспондеров{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-warning text-white">
<h2 class="mb-0">Загрузка данных транспондеров из CellNet</h2>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert {{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<p class="card-text">Загрузите xml-файл и выберите спутник для загрузки данных в базу.</p>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.file.id_for_label }}" class="form-label">Выберите xml файл:</label>
{{ form.file }}
{% if form.file.errors %}
<div class="text-danger mt-1">{{ form.file.errors }}</div>
{% endif %}
<div class="form-text">Загрузите xml-файл (.xml) с данными для обработки</div>
</div>
{% comment %} <div class="mb-3">
<label for="{{ form.sat_choice.id_for_label }}" class="form-label">Выберите спутник:</label>
{{ form.sat_choice }}
{% if form.sat_choice.errors %}
<div class="text-danger mt-1">{{ form.sat_choice.errors }}</div>
{% endif %}
</div> {% endcomment %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'home' %}" class="btn btn-secondary me-md-2">Назад</a>
<button type="submit" class="btn btn-warning">Добавить в базу</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -15,6 +15,7 @@ urlpatterns = [
path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),
path('kubsat-excel/', views.ProcessKubsatView.as_view(), name='kubsat_excel'),
# path('upload/', views.upload_file, name='upload_file'),
]

View File

@@ -10,12 +10,18 @@ from .models import (
ObjItem,
CustomUser
)
from mapsapp.models import Transponders
from datetime import datetime, time
import pandas as pd
import numpy as np
from django.contrib.gis.geos import Point
import json
import re
import io
from django.db.models import F, Count, Exists, OuterRef, Min, Max
from geopy.geocoders import Nominatim
import reverse_geocoder as rg
import time
def get_all_constants():
sats = [sat.name for sat in Satellite.objects.all()]
@@ -74,7 +80,10 @@ def fill_data_from_df(df: pd.DataFrame, sat: Satellite):
freq = remove_str(stroka[1]['Частота, МГц'])
freq_line = remove_str(stroka[1]['Полоса, МГц'])
v = remove_str(stroka[1]['Символьная скорость, БОД'])
try:
mod_obj, _ = Modulation.objects.get_or_create(name=stroka[1]['Модуляция'].strip())
except AttributeError:
mod_obj, _ = Modulation.objects.get_or_create(name='-')
snr = remove_str(stroka[1]['ОСШ'])
date = stroka[1]['Дата'].date()
time_ = stroka[1]['Время']
@@ -192,17 +201,7 @@ def get_point_from_json(filepath: str):
def get_points_from_csv(file_content):
import io
if hasattr(file_content, 'read'):
content = file_content.read()
if isinstance(content, bytes):
content = content.decode('utf-8')
else:
if isinstance(file_content, bytes):
content = content.decode('utf-8')
else:
content = file_content
df = pd.read_csv(io.StringIO(content), sep=";",
df = pd.read_csv(io.StringIO(file_content), sep=";",
names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3'])
df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float)
df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
@@ -266,73 +265,22 @@ def get_points_from_csv(file_content):
}
)
obj_item_obj.save()
# df = pd.read_csv(filepath, sep=";",
# names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3'])
# df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float)
# df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
# for row in df.iterrows():
# row = row[1]
# match row['obj'].split(' ')[-1]:
# case 'V':
# pol = 'Вертикальная'
# case 'H':
# pol = 'Горизонтальная'
# case 'R':
# pol = 'Правая'
# case 'L':
# pol = 'Левая'
# case _:
# pol = '-'
# pol_obj, _ = Polarization.objects.get_or_create(
# name=pol
# )
# sat_obj, _ = Satellite.objects.get_or_create(
# name=row['sat'],
# defaults={'norad': row['norad_id']}
# )
# mir_1_obj, _ = Mirror.objects.get_or_create(
# name=row['mir_1']
# )
# mir_2_obj, _ = Mirror.objects.get_or_create(
# name=row['mir_2']
# )
# mir_lst = [row['mir_1'], row['mir_2']]
# if not pd.isna(row['mir_3']):
# mir_3_obj, _ = Mirror.objects.get_or_create(
# name=row['mir_3']
# )
# vch_load_obj, _ = Parameter.objects.get_or_create(
# id_satellite=sat_obj,
# polarization=pol_obj,
# frequency=row['freq'],
# freq_range=row['f_range'],
# defaults={'id_user_add': CustomUser.objects.get(id=1)}
# )
# geo_obj, _ = Geo.objects.get_or_create(
# timestamp=row['time'],
# coords=Point(row['lon'], row['lat'], srid=4326),
# defaults={
# 'is_average': False,
# 'id_user_add': CustomUser.objects.get(id=1),
# }
# )
# geo_obj.mirrors.set(Mirror.objects.filter(name__in=mir_lst))
# obj_item_obj, _ = ObjItem.objects.get_or_create(
# name=row['obj'],
# # id_satellite=sat_obj,
# id_vch_load=vch_load_obj,
# id_geo=geo_obj,
# defaults={
# 'id_user_add': CustomUser.objects.get(id=1)
# }
# )
# obj_item_obj.save()
def get_vch_load_from_html(file, sat: Satellite) -> None:
filename = file.name.split('_')
transfer = filename[3]
match filename[2]:
case 'H':
pol = 'Горизонтальная'
case 'V':
pol = 'Вертикальная'
case 'R':
pol = 'Правая'
case 'L':
pol = 'Левая'
case _:
pol = '-'
tables = pd.read_html(file, encoding='windows-1251')
df = tables[0]
df = df.drop(0).reset_index(drop=True)
@@ -362,6 +310,10 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
else:
pack = None
polarization, _ = Polarization.objects.get_or_create(
name=pol
)
mod, _ = Modulation.objects.get_or_create(
name=value['Модуляция']
)
@@ -372,7 +324,10 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
id_satellite=sat,
frequency=value['Частота, МГц'],
freq_range=value['Полоса, МГц'],
polarization=polarization,
defaults={
"transfer": float(transfer),
# "polarization": polarization,
"status": value['Статус'],
"power": value['Мощность, дБм'],
"bod_velocity": bod_velocity,
@@ -386,15 +341,6 @@ def get_vch_load_from_html(file, sat: Satellite) -> None:
)
sigma_load.save()
def define_ku_transfer(min_freq: float, max_freq: float) -> int | None:
fss = (10700, 11700)
dss = (11700, 12750)
if min_freq + 9750 >= fss[0] and max_freq + 9750 <= fss[1]:
return 9750
elif min_freq + 10750 >= dss[0] and max_freq + 10750 <= dss[1]:
return 10750
return None
def compare_and_link_vch_load(sat_id: Satellite, eps_freq: float, eps_frange: float, ku_range: float):
item_obj = ObjItem.objects.filter(id_vch_load__id_satellite=sat_id)
vch_sigma = SigmaParameter.objects.filter(id_satellite=sat_id)
@@ -406,10 +352,62 @@ def compare_and_link_vch_load(sat_id: Satellite, eps_freq: float, eps_frange: fl
continue
# if unique_points = Point.objects.order_by('frequency').distinct('frequency')
for sigma in vch_sigma:
if abs(sigma.frequency + ku_range - vch_load.frequency) <= vch_load.frequency*eps_freq/100 and abs(sigma.freq_range - vch_load.freq_range) <= vch_load.freq_range*eps_frange/100:
if (
abs(sigma.transfer_frequency - vch_load.frequency) <= vch_load.frequency*eps_freq/100 and
abs(sigma.freq_range - vch_load.freq_range) <= vch_load.freq_range*eps_frange/100 and
sigma.polarization == vch_load.polarization
):
sigma.parameter = vch_load
sigma.save()
link_count += 1
return obj_count, link_count
def kub_report(data_in: io.StringIO) -> pd.DataFrame:
df_in = pd.read_excel(data_in)
df = pd.DataFrame(columns=['Дата', 'Широта', 'Долгота',
'Высота', 'Населённый пункт', 'ИСЗ',
'Прямой канал, МГц', 'Обратный канал, МГц', 'Перенос, МГц', 'Полоса, МГц', 'Зеркала'])
for row in df_in.iterrows():
value = row[1]
date = datetime.date(datetime.now())
lat = value['Широта, град']
lon = value['Долгота, град']
isz = value['ИСЗ']
downlink = value['Обратный канал, МГц']
freq_range = value['Полоса, МГц']
norad = int(re.findall(r'\((\d+)\)', isz)[0])
sat_obj = Satellite.objects.get(norad=norad)
pol_obj = Polarization.objects.get(name=value['Поляризация'].strip())
transponder = Transponders.objects.filter(
sat_id=sat_obj,
polarization=pol_obj,
downlink__gte=downlink - F('frequency_range')/2,
downlink__lte=downlink + F('frequency_range')/2,
).first()
# try:
# location = geolocator.reverse(f"{lat}, {lon}", language="ru").raw['address']
# loc_name = location.get('city', '') or location.get('town', '') or location.get('province', '') or location.get('country', '')
# except AttributeError:
# loc_name = ''
# time.sleep(1)
loc_name = ''
if transponder: #and not (len(transponder) > 1):
transfer = transponder.transfer
uplink = transfer + downlink
new_row = pd.DataFrame([{'Дата': date,
'Широта': lat,
'Долгота': lon,
'Высота': 0.0,
'Населённый пункт': loc_name,
'ИСЗ': isz,
'Прямой канал, МГц': uplink,
'Обратный канал, МГц': downlink,
'Перенос, МГц': transfer,
'Полоса, МГц': freq_range,
'Зеркала': ''
}])
df = pd.concat([df, new_row], ignore_index=True)
else:
print("Ничего не найдено в транспондерах")
return df

View File

@@ -1,6 +1,6 @@
from django.shortcuts import render, redirect
from django.contrib import messages
from django.http import JsonResponse
from django.http import JsonResponse, HttpResponse
from django.views.decorators.http import require_GET
from django.contrib.admin.views.decorators import staff_member_required
from django.utils.decorators import method_decorator
@@ -13,13 +13,15 @@ from .utils import (
add_satellite_list,
get_points_from_csv,
get_vch_load_from_html,
compare_and_link_vch_load
compare_and_link_vch_load,
kub_report
)
from mapsapp.utils import parse_transponders_from_json
from .forms import LoadExcelData, LoadCsvData, UploadFileForm, VchLinkForm
from mapsapp.utils import parse_transponders_from_json, parse_transponders_from_xml
from .forms import LoadExcelData, LoadCsvData, UploadFileForm, VchLinkForm, UploadVchLoad, NewEventForm
from .models import ObjItem
from .clusters import get_clusters
from dbapp.settings import BASE_DIR
from io import BytesIO
class AddSatellitesView(View):
@@ -27,13 +29,33 @@ class AddSatellitesView(View):
add_satellite_list()
return redirect('home')
class AddTranspondersView(View):
def get(self, request):
# class AddTranspondersView(View):
# def get(self, request):
# try:
# parse_transponders_from_json(BASE_DIR / "transponders.json")
# except FileNotFoundError:
# print("Файл не найден")
# return redirect('home')
class AddTranspondersView(FormView):
template_name = 'mainapp/transponders_upload.html'
form_class = UploadFileForm
def form_valid(self, form):
uploaded_file = self.request.FILES['file']
try:
parse_transponders_from_json(BASE_DIR / "transponders.json")
except FileNotFoundError:
print("Файл не найден")
return redirect('home')
content = uploaded_file.read()
parse_transponders_from_xml(BytesIO(content))
messages.success(self.request, "Файл успешно обработан")
except ValueError as e:
messages.error(self.request, f"Ошибка при чтении таблиц: {e}")
except Exception as e:
messages.error(self.request, f"Неизвестная ошибка: {e}")
return redirect('add_trans')
def form_invalid(self, form):
messages.error(self.request, "Форма заполнена некорректно.")
return super().form_invalid(form)
class HomePageView(TemplateView):
template_name = 'mainapp/home.html'
@@ -118,22 +140,7 @@ class LoadCsvDataView(FormView):
messages.error(self.request, "Форма заполнена некорректно.")
return super().form_invalid(form)
# def upload_file(request):
# if request.method == 'POST' and request.FILES:
# form = UploadFileForm(request.POST, request.FILES)
# if form.is_valid():
# uploaded_file = request.FILES['file']
# # Обработка текстового файла, например:
# df = pd.read_csv(uploaded_file)
# df = pd.read_csv(filepath, sep=";",
# names=['id', 'obj', 'lat', 'lon', 'h', 'time', 'sat', 'norad_id', 'freq', 'f_range', 'et', 'qaul', 'mir_1', 'mir_2', 'mir_3'])
# df[['lat', 'lon', 'freq', 'f_range']] = df[['lat', 'lon', 'freq', 'f_range']].replace(',', '.', regex=True).astype(float)
# df['time'] = pd.to_datetime(df['time'], format='%d.%m.%Y %H:%M:%S')
# get_points_from_csv(df)
# return JsonResponse({'status': 'success'})
# else:
# return JsonResponse({'status': 'error', 'errors': form.errors}, status=400)
# return render(request, 'mainapp/add_data_from_csv.html')
from collections import defaultdict
@method_decorator(staff_member_required, name='dispatch')
@@ -162,7 +169,6 @@ class ShowMapView(UserPassesTestMixin, View):
'frequency': p["freq"]
})
# Преобразуем в список словарей для удобства в шаблоне
groups = [
{
"name": name,
@@ -190,7 +196,7 @@ class ClusterTestView(View):
class UploadVchLoadView(FormView):
template_name = 'mainapp/upload_html.html'
form_class = UploadFileForm
form_class = UploadVchLoad
def form_valid(self, form):
selected_sat = form.cleaned_data['sat_choice']
@@ -225,3 +231,37 @@ class LinkVchSigmaView(FormView):
def form_invalid(self, form):
return self.render_to_response(self.get_context_data(form=form))
class ProcessKubsatView(FormView):
template_name = 'mainapp/process_kubsat.html'
form_class = NewEventForm
def form_valid(self, form):
# selected_sat = form.cleaned_data['sat_choice']
# selected_pol = form.cleaned_data['pol_choice']
uploaded_file = self.request.FILES['file']
try:
content = uploaded_file.read()
df = kub_report(BytesIO(content))
output = BytesIO()
with pd.ExcelWriter(output, engine='openpyxl') as writer:
df.to_excel(writer, index=False, sheet_name='Результат')
output.seek(0)
response = HttpResponse(
output.getvalue(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
response['Content-Disposition'] = f'attachment; filename="kubsat_report.xlsx"'
messages.success(self.request, "Событие успешно обработано!")
return response
except Exception as e:
messages.error(self.request, f"Ошибка при обработке файла: {str(e)}")
return redirect('kubsat_excel')
# return redirect('kubsat_excel')
def form_invalid(self, form):
messages.error(self.request, "Форма заполнена некорректно.")
return super().form_invalid(form)

View File

@@ -5,20 +5,24 @@ from more_admin_filters import MultiSelectDropdownFilter, MultiSelectFilter, Mul
from import_export.admin import ImportExportActionModelAdmin
@admin.register(Transponders)
class PolarizationAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
class TranspondersAdmin(ImportExportActionModelAdmin, admin.ModelAdmin):
list_display = (
"sat_id",
"name",
"zone_name",
"frequency",
"downlink",
"uplink",
"frequency_range",
"transfer",
"polarization",
)
list_filter = (
("polarization", MultiSelectRelatedDropdownFilter),
("sat_id", MultiSelectRelatedDropdownFilter),
("frequency", NumericRangeFilterBuilder()),
# ("frequency", NumericRangeFilterBuilder()),
"zone_name"
)
search_fields = ("name",)
search_fields = ("name", "sat_id__name")
ordering = ("name",)
# def sat_name(self, obj):
# return

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.2.7 on 2025-10-27 12:20
import django.db.models.expressions
import django.db.models.functions.math
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mapsapp', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='transponders',
name='frequency',
),
migrations.AddField(
model_name='transponders',
name='downlink',
field=models.FloatField(blank=True, null=True, verbose_name='Downlink'),
),
migrations.AddField(
model_name='transponders',
name='uplink',
field=models.FloatField(blank=True, null=True, verbose_name='Uplink'),
),
migrations.AddField(
model_name='transponders',
name='transfer',
field=models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Расстояние между купсатом и гео, км'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-10-27 13:10
import django.db.models.expressions
import django.db.models.functions.math
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mapsapp', '0002_remove_transponders_frequency_transponders_downlink_and_more'),
]
operations = [
migrations.AlterField(
model_name='transponders',
name='transfer',
field=models.GeneratedField(db_persist=True, expression=models.ExpressionWrapper(django.db.models.functions.math.Abs(django.db.models.expressions.CombinedExpression(models.F('downlink'), '-', models.F('uplink'))), output_field=models.FloatField()), null=True, output_field=models.FloatField(), verbose_name='Перенос'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-28 05:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mapsapp', '0003_alter_transponders_transfer'),
]
operations = [
migrations.AlterField(
model_name='transponders',
name='zone_name',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Название зоны'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-29 14:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mapsapp', '0004_alter_transponders_zone_name'),
]
operations = [
migrations.AlterField(
model_name='transponders',
name='frequency_range',
field=models.FloatField(blank=True, null=True, verbose_name='Полоса'),
),
]

View File

@@ -1,15 +1,27 @@
from django.db import models
from mainapp.models import Satellite, Polarization, get_default_polarization
from django.db.models import F, ExpressionWrapper
from django.db.models.functions import Abs
class Transponders(models.Model):
name = models.CharField(max_length=30, null=True, blank=True, verbose_name="Название транспондера")
frequency = models.FloatField(blank=True, null=True, verbose_name="Центральная частота")
frequency_range = models.FloatField(blank=True, null=True, verbose_name="Полоса частот")
zone_name = models.CharField(max_length=60, blank=True, null=True, verbose_name="Название зоны")
downlink = models.FloatField(blank=True, null=True, verbose_name="Downlink")
frequency_range = models.FloatField(blank=True, null=True, verbose_name="Полоса")
uplink = models.FloatField(blank=True, null=True, verbose_name="Uplink")
zone_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Название зоны")
polarization = models.ForeignKey(
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="tran_polarizations", null=True, blank=True, verbose_name="Поляризация"
)
sat_id = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="tran_satellite", verbose_name="Спутник")
transfer =models.GeneratedField(
expression=ExpressionWrapper(
Abs(F('downlink') - F('uplink')),
output_field=models.FloatField()
),
output_field=models.FloatField(),
db_persist=True,
null=True, blank=True, verbose_name="Перенос"
)
def __str__(self):
return self.name

View File

@@ -3,6 +3,7 @@ import re
import json
from .models import Transponders
from mainapp.models import Polarization, Satellite
from io import BytesIO
def search_satellite_on_page(data: dict, satellite_name: str):
for pos, value in data.get('page', {}).get('positions').items():
@@ -90,3 +91,68 @@ def parse_transponders_from_json(filepath: str):
)
tran_obj.save()
from lxml import etree
def parse_transponders_from_xml(data_in: BytesIO):
tree = etree.parse(data_in)
ns = {
'i': 'http://www.w3.org/2001/XMLSchema-instance',
'ns': 'http://schemas.datacontract.org/2004/07/Geolocation.Domain.Utils.Repository.SatellitesSerialization.Memos',
'tr': 'http://schemas.datacontract.org/2004/07/Geolocation.Common.Extensions'
}
satellites = tree.xpath('//ns:SatelliteMemo', namespaces=ns)
for sat in satellites[:]:
name = sat.xpath('./ns:name/text()', namespaces=ns)[0]
if name == 'X' or 'DONT USE' in name:
continue
norad = sat.xpath('./ns:norad/text()', namespaces=ns)
beams = sat.xpath('.//ns:BeamMemo', namespaces=ns)
zones = {}
for zone in beams:
zone_name = zone.xpath('./ns:name/text()', namespaces=ns)[0] if zone.xpath('./ns:name/text()', namespaces=ns) else '-'
zones[zone.xpath('./ns:id/text()', namespaces=ns)[0]] = {
"name": zone_name,
"pol": zone.xpath('./ns:polarization/text()', namespaces=ns)[0],
}
transponders = sat.xpath('.//ns:TransponderMemo', namespaces=ns)
for transponder in transponders:
tr_id = transponder.xpath('./ns:downlinkBeamId/text()', namespaces=ns)[0]
downlink_start = float(transponder.xpath('./ns:downlinkFrequency/tr:start/text()', namespaces=ns)[0])
downlink_end = float(transponder.xpath('./ns:downlinkFrequency/tr:end/text()', namespaces=ns)[0])
uplink_start = float(transponder.xpath('./ns:uplinkFrequency/tr:start/text()', namespaces=ns)[0])
uplink_end = float(transponder.xpath('./ns:uplinkFrequency/tr:end/text()', namespaces=ns)[0])
tr_data = zones[tr_id]
# p = tr_data['pol'][0] if tr_data['pol'] else '-'
match tr_data['pol']:
case 'Horizontal':
pol = 'Горизонтальная'
case 'Vertical':
pol = 'Вертикальная'
case 'CircularRight':
pol = 'Правая'
case 'CircularLeft':
pol = 'Левая'
case _:
pol = '-'
tr_name = transponder.xpath('./ns:name/text()', namespaces=ns)[0]
pol_obj, _ = Polarization.objects.get_or_create(name=pol)
sat_obj, _ = Satellite.objects.get_or_create(
name=name,
defaults={
"norad": int(norad[0]) if norad else -1
})
trans_obj, _ = Transponders.objects.get_or_create(
polarization=pol_obj,
downlink=(downlink_start+downlink_end)/2/1000000,
uplink=(uplink_start+uplink_end)/2/1000000,
frequency_range=abs(downlink_end-downlink_start)/1000000,
name=tr_name,
defaults={
"zone_name": tr_data['name'],
"sat_id": sat_obj,
}
)
trans_obj.save()

View File

@@ -19,7 +19,9 @@ dependencies = [
"django-leaflet>=0.32.0",
"django-map-widgets>=0.5.1",
"django-more-admin-filters>=1.13",
"gdal",
"dotenv>=0.9.9",
"geopy>=2.4.1",
"gunicorn>=23.0.0",
"lxml>=6.0.2",
"matplotlib>=3.10.7",
"numpy>=2.3.3",
@@ -28,9 +30,11 @@ dependencies = [
"psycopg>=3.2.10",
"redis>=6.4.0",
"requests>=2.32.5",
"reverse-geocoder>=1.5.1",
"scikit-learn>=1.7.2",
"setuptools>=80.9.0",
]
[tool.uv.sources]
gdal = { path = "gdal-3.10.2-cp313-cp313-win_amd64.whl" }
[dependency-groups]
dev = []

78
dbapp/uv.lock generated
View File

@@ -212,7 +212,9 @@ dependencies = [
{ name = "django-leaflet" },
{ name = "django-map-widgets" },
{ name = "django-more-admin-filters" },
{ name = "gdal" },
{ name = "dotenv" },
{ name = "geopy" },
{ name = "gunicorn" },
{ name = "lxml" },
{ name = "matplotlib" },
{ name = "numpy" },
@@ -221,6 +223,7 @@ dependencies = [
{ name = "psycopg" },
{ name = "redis" },
{ name = "requests" },
{ name = "reverse-geocoder" },
{ name = "scikit-learn" },
{ name = "setuptools" },
]
@@ -241,7 +244,9 @@ requires-dist = [
{ name = "django-leaflet", specifier = ">=0.32.0" },
{ name = "django-map-widgets", specifier = ">=0.5.1" },
{ name = "django-more-admin-filters", specifier = ">=1.13" },
{ name = "gdal", path = "gdal-3.10.2-cp313-cp313-win_amd64.whl" },
{ name = "dotenv", specifier = ">=0.9.9" },
{ name = "geopy", specifier = ">=2.4.1" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "lxml", specifier = ">=6.0.2" },
{ name = "matplotlib", specifier = ">=3.10.7" },
{ name = "numpy", specifier = ">=2.3.3" },
@@ -250,10 +255,14 @@ requires-dist = [
{ name = "psycopg", specifier = ">=3.2.10" },
{ name = "redis", specifier = ">=6.4.0" },
{ name = "requests", specifier = ">=2.32.5" },
{ name = "reverse-geocoder", specifier = ">=1.5.1" },
{ name = "scikit-learn", specifier = ">=1.7.2" },
{ name = "setuptools", specifier = ">=80.9.0" },
]
[package.metadata.requires-dev]
dev = []
[[package]]
name = "diff-match-patch"
version = "20241021"
@@ -410,6 +419,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/87/7c/4b261b96b357d94ef267f39856ef0bb72a33f078a38bd22ee96d168fe272/django_more_admin_filters-1.13-py3-none-any.whl", hash = "sha256:df4d46e4b589566b85f149ea5b7558c6cc4ae22b0d264973f8d4a2d478ef5120", size = 147360, upload-time = "2025-06-06T11:26:42.964Z" },
]
[[package]]
name = "dotenv"
version = "0.9.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dotenv" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
]
[[package]]
name = "et-xmlfile"
version = "2.0.0"
@@ -453,16 +473,37 @@ wheels = [
]
[[package]]
name = "gdal"
version = "3.10.2"
source = { path = "gdal-3.10.2-cp313-cp313-win_amd64.whl" }
name = "geographiclib"
version = "2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/78/4892343230a9d29faa1364564e525307a37e54ad776ea62c12129dbba704/geographiclib-2.1.tar.gz", hash = "sha256:6a6545e6262d0ed3522e13c515713718797e37ed8c672c31ad7b249f372ef108", size = 37004, upload-time = "2025-08-21T21:34:26Z" }
wheels = [
{ filename = "gdal-3.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:d6aae781b9847065f831f3457c6c01d0b9272818656031d723dc88c160a8ec26" },
{ url = "https://files.pythonhosted.org/packages/31/b3/802576f2ea5dcb48501bb162e4c7b7b3ca5654a42b2c968ef98a797a4c79/geographiclib-2.1-py3-none-any.whl", hash = "sha256:e2a873b9b9e7fc38721ad73d5f4e6c9ed140d428a339970f505c07056997d40b", size = 40740, upload-time = "2025-08-21T21:34:24.955Z" },
]
[package.metadata]
requires-dist = [{ name = "numpy", marker = "extra == 'numpy'", specifier = ">1.0.0" }]
provides-extras = ["numpy"]
[[package]]
name = "geopy"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "geographiclib" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0e/fd/ef6d53875ceab72c1fad22dbed5ec1ad04eb378c2251a6a8024bad890c3b/geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1", size = 117625, upload-time = "2023-11-23T21:49:32.734Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/15/cf2a69ade4b194aa524ac75112d5caac37414b20a3a03e6865dfe0bd1539/geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7", size = 125437, upload-time = "2023-11-23T21:49:30.421Z" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
]
[[package]]
name = "idna"
@@ -851,6 +892,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
[[package]]
name = "python-slugify"
version = "8.0.4"
@@ -896,6 +946,16 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "reverse-geocoder"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "scipy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/0f/b7d5d4b36553731f11983e19e1813a1059ad0732c5162c01b3220c927d31/reverse_geocoder-1.5.1.tar.gz", hash = "sha256:2a2e781b5f69376d922b78fe8978f1350c84fce0ddb07e02c834ecf98b57c75c", size = 2246559, upload-time = "2016-09-15T16:46:46.277Z" }
[[package]]
name = "scikit-learn"
version = "1.7.2"