Сделал парсер, начал интеграцию с бд

This commit is contained in:
2025-11-07 16:45:00 +03:00
parent 439ca6407f
commit 331a9e41cb
16 changed files with 1031 additions and 90 deletions

View File

View File

@@ -0,0 +1,8 @@
from django.contrib import admin
from .models import LyngSat
@admin.register(LyngSat)
class LyngSatAdmin(admin.ModelAdmin):
list_display = ("mark", "timestamp")
search_fields = ("mark", )
ordering = ("timestamp",)

6
dbapp/lyngsatapp/apps.py Normal file
View File

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

View File

View File

@@ -0,0 +1,36 @@
from django.db import models
from mainapp.models import (
Satellite,
Polarization,
Modulation,
Standard,
get_default_polarization,
get_default_modulation,
get_default_standard
)
class LyngSat(models.Model):
id_satellite = models.ForeignKey(Satellite, on_delete=models.PROTECT, related_name="lyngsat", verbose_name="Спутник", null=True)
polarization = models.ForeignKey(
Polarization, default=get_default_polarization, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Поляризация"
)
modulation = models.ForeignKey(
Modulation, default=get_default_modulation, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Модуляция"
)
standard = models.ForeignKey(
Standard, default=get_default_standard, on_delete=models.SET_DEFAULT, related_name="lyngsat", null=True, blank=True, verbose_name="Стандарт"
)
frequency = models.FloatField(default=0, null=True, blank=True, verbose_name="Частота, МГц")
sym_velocity = models.FloatField(default=0, null=True, blank=True, verbose_name="Символьная скорость, БОД")
last_update = models.DateTimeField(null=True, blank=True, verbose_name="Время")
channel_info = models.CharField(max_length=20, blank=True, null=True, verbose_name="Описание источника")
# url = models.URLField(max_length = 200, blank=True, null=True, verbose_name="Ссылка на страницу")
def __str__(self):
return f"Ист {self.frequency}, {self.polarization}"
class Meta:
verbose_name = "Источник LyngSat"
verbose_name_plural = "Источники LyngSat"

371
dbapp/lyngsatapp/parser.py Normal file
View File

@@ -0,0 +1,371 @@
import requests
from bs4 import BeautifulSoup
from datetime import datetime
import re
import time
class LyngSatParser:
"""Парсер данных для LyngSat(Для работы нужен flaresolver)"""
def __init__(
self,
flaresolver_url: str = "http://localhost:8191/v1",
regions: list[str] | None = None,
target_sats: list[str] | None = None,
):
self.flaresolver_url = flaresolver_url
self.regions = regions
self.target_sats = list(map(lambda sat: sat.strip().lower(), target_sats)) if regions else None
self.regions = regions if regions else ["europe", "asia", "america", "atlantic"]
self.BASE_URL = "https://www.lyngsat.com"
def parse_metadata(self, metadata: str) -> dict:
if not metadata or not metadata.strip():
return {
'standard': None,
'modulation': None,
'symbol_rate': None,
'fec': None
}
normalized = re.sub(r'\s+', '', metadata.strip())
fec_match = re.search(r'([1-9]/[1-9])$', normalized)
fec = fec_match.group(1) if fec_match else None
if fec_match:
core = normalized[:fec_match.start()]
else:
core = normalized
std_match = re.match(r'(DVB-S2?|ABS-S|DVB-T2?|ATSC|ISDB)', core)
standard = std_match.group(1) if std_match else None
rest = core[len(standard):] if standard else core
modulation = None
mod_match = re.match(r'(8PSK|QPSK|16APSK|32APSK|64QAM|256QAM|BPSK)', rest)
if mod_match:
modulation = mod_match.group(1)
rest = rest[len(modulation):]
symbol_rate = None
sr_match = re.search(r'(\d+)$', rest)
if sr_match:
try:
symbol_rate = int(sr_match.group(1))
except ValueError:
pass
return {
'standard': standard,
'modulation': modulation,
'symbol_rate': symbol_rate,
'fec': fec
}
def extract_date(self, s: str) -> datetime | None:
s = s.strip()
match = re.search(r'(\d{6})$', s)
if not match:
return None
yymmdd = match.group(1)
try:
return datetime.strptime(yymmdd, '%y%m%d').date()
except ValueError:
return None
def convert_polarization(self, polarization: str) -> str:
"""Преобразовать код поляризации в понятное название на русском"""
polarization_map = {
'V': 'Вертикальная',
'H': 'Горизонтальная',
'R': 'Правая',
'L': 'Левая'
}
return polarization_map.get(polarization.upper(), polarization)
def get_region_pages(self) -> list[str]:
html_regions = []
for region in self.regions:
url = f"{self.BASE_URL}/{region}.html"
payload = {
"cmd": "request.get",
"url": url,
"maxTimeout": 60000
}
response = requests.post(self.flaresolver_url, json=payload)
if response.status_code != 200:
continue
html_content = response.json().get("solution", {}).get("response", "")
html_regions.append(html_content)
print(f"Обработал страницу по {region}")
return html_regions
def get_satellites_data(self) -> dict[dict]:
sat_data = {}
for region_page in self.get_region_pages():
soup = BeautifulSoup(region_page, "html.parser")
col_table = soup.find_all("div", class_="desktab")[0]
tables = col_table.find_next_sibling('table').find_all('table')
trs = []
for table in tables:
trs.extend(table.find_all('tr'))
for tr in trs:
sat_name = tr.find('span').text
if self.target_sats is not None:
if sat_name.strip().lower() not in self.target_sats:
continue
try:
sat_url = tr.find_all('a')[2]['href']
except IndexError:
sat_url = tr.find_all('a')[0]['href']
update_date = tr.find_all('td')[-1].text
sat_response = requests.post(self.flaresolver_url, json={
"cmd": "request.get",
"url": f"{self.BASE_URL}/{sat_url}",
"maxTimeout": 60000
})
html_content = sat_response.json().get("solution", {}).get("response", "")
sat_page_data = self.get_satellite_content(html_content)
sat_data[sat_name] = {
"url": f"{self.BASE_URL}/{sat_url}",
"update_date": datetime.strptime(update_date, "%y%m%d").date(),
"sources": sat_page_data
}
return sat_data
def get_satellite_content(self, html_content: str) -> dict:
sat_soup = BeautifulSoup(html_content, "html.parser")
big_table = sat_soup.find('table', class_='bigtable')
all_tables = big_table.find_all("div", class_="desktab")[:-1]
data = []
for table in all_tables:
trs = table.find_next_sibling('table').find_all('tr')
for idx, tr in enumerate(trs):
tds = tr.find_all('td')
if len(tds) < 9 or idx < 2:
continue
freq, polarization = tds[0].find('b').text.strip().split('\xa0')
polarization = self.convert_polarization(polarization)
meta = self.parse_metadata(tds[1].text)
provider_name = tds[3].text
last_update = self.extract_date(tds[-1].text)
data.append({
"freq": freq,
"pol": polarization,
"metadata": meta,
"provider_name": provider_name,
"last_update": last_update
})
return data
class KingOfSatParser:
def __init__(self, base_url="https://ru.kingofsat.net", max_satellites=0):
"""
Инициализация парсера
:param base_url: Базовый URL сайта
:param max_satellites: Максимальное количество спутников для парсинга (0 - все)
"""
self.base_url = base_url
self.max_satellites = max_satellites
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
})
def convert_polarization(self, polarization):
"""Преобразовать код поляризации в понятное название на русском"""
polarization_map = {
'V': 'Вертикальная',
'H': 'Горизонтальная',
'R': 'Правая',
'L': 'Левая'
}
return polarization_map.get(polarization.upper(), polarization)
def fetch_page(self, url):
"""Получить HTML страницу"""
try:
response = self.session.get(url, timeout=30)
response.raise_for_status()
return response.text
except Exception as e:
print(f"Ошибка при получении страницы {url}: {e}")
return None
def parse_satellite_table(self, html_content):
"""Распарсить таблицу со спутниками"""
soup = BeautifulSoup(html_content, 'html.parser')
satellites = []
table = soup.find('table')
if not table:
print("Таблица не найдена")
return satellites
rows = table.find_all('tr')[1:]
for row in rows:
cols = row.find_all('td')
if len(cols) < 13:
continue
try:
position_cell = cols[0].text.strip()
position_match = re.search(r'([\d\.]+)°([EW])', position_cell)
if position_match:
position_value = position_match.group(1)
position_direction = position_match.group(2)
position = f"{position_value}{position_direction}"
else:
position = None
# Название спутника (2-я колонка)
satellite_cell = cols[1]
satellite_name = satellite_cell.get_text(strip=True)
# Удаляем возможные лишние символы или пробелы
satellite_name = re.sub(r'\s+', ' ', satellite_name).strip()
# NORAD (3-я колонка)
norad = cols[2].text.strip()
if not norad or norad == "-":
norad = None
ini_link = None
ini_cell = cols[3]
ini_img = ini_cell.find('img', src=lambda x: x and 'disquette.gif' in x)
if ini_img and position:
ini_link = f"https://ru.kingofsat.net/dl.php?pos={position}&fkhz=0"
update_date = cols[12].text.strip() if len(cols) > 12 else None
if satellite_name and ini_link and position:
satellites.append({
'position': position,
'name': satellite_name,
'norad': norad,
'ini_url': ini_link,
'update_date': update_date
})
except Exception as e:
print(f"Ошибка при обработке строки таблицы: {e}")
continue
return satellites
def parse_ini_file(self, ini_content):
"""Распарсить содержимое .ini файла"""
data = {
'metadata': {},
'sattype': {},
'dvb': {}
}
# # Извлекаем метаданные из комментариев
# metadata_match = re.search(r'\[ downloaded from www\.kingofsat\.net \(c\) (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \]', ini_content)
# if metadata_match:
# data['metadata']['downloaded'] = metadata_match.group(1)
# Парсим секцию [SATTYPE]
sattype_match = re.search(r'\[SATTYPE\](.*?)\n\[', ini_content, re.DOTALL)
if sattype_match:
sattype_content = sattype_match.group(1).strip()
for line in sattype_content.split('\n'):
line = line.strip()
if '=' in line:
key, value = line.split('=', 1)
data['sattype'][key.strip()] = value.strip()
# Парсим секцию [DVB]
dvb_match = re.search(r'\[DVB\](.*?)(?:\n\[|$)', ini_content, re.DOTALL)
if dvb_match:
dvb_content = dvb_match.group(1).strip()
for line in dvb_content.split('\n'):
line = line.strip()
if '=' in line:
key, value = line.split('=', 1)
params = [p.strip() for p in value.split(',')]
polarization = params[1] if len(params) > 1 else ''
if polarization:
polarization = self.convert_polarization(polarization)
data['dvb'][key.strip()] = {
'frequency': params[0] if len(params) > 0 else '',
'polarization': polarization,
'symbol_rate': params[2] if len(params) > 2 else '',
'fec': params[3] if len(params) > 3 else '',
'standard': params[4] if len(params) > 4 else '',
'modulation': params[5] if len(params) > 5 else ''
}
return data
def download_ini_file(self, url):
"""Скачать содержимое .ini файла"""
try:
response = self.session.get(url, timeout=30)
response.raise_for_status()
return response.text
except Exception as e:
print(f"Ошибка при скачивании .ini файла {url}: {e}")
return None
def get_all_satellites_data(self):
"""Получить данные всех спутников с учетом ограничения max_satellites"""
html_content = self.fetch_page(self.base_url + '/satellites')
if not html_content:
return []
satellites = self.parse_satellite_table(html_content)
if self.max_satellites > 0 and len(satellites) > self.max_satellites:
satellites = satellites[:self.max_satellites]
results = []
processed_count = 0
for satellite in satellites:
print(f"Обработка спутника: {satellite['name']} ({satellite['position']})")
ini_content = self.download_ini_file(satellite['ini_url'])
if not ini_content:
print(f"Не удалось скачать .ini файл для {satellite['name']}")
continue
parsed_ini = self.parse_ini_file(ini_content)
result = {
'satellite_name': satellite['name'],
'position': satellite['position'],
'norad': satellite['norad'],
'update_date': satellite['update_date'],
'ini_url': satellite['ini_url'],
'ini_data': parsed_ini
}
results.append(result)
processed_count += 1
if self.max_satellites > 0 and processed_count >= self.max_satellites:
break
time.sleep(1)
return results
def create_satellite_dict(self, satellites_data):
"""Создать словарь с данными спутников"""
satellite_dict = {}
for data in satellites_data:
key = f"{data['position']}_{data['satellite_name'].replace(' ', '_').replace('/', '_')}"
satellite_dict[key] = {
'name': data['satellite_name'],
'position': data['position'],
'norad': data['norad'],
'update_date': data['update_date'],
'ini_url': data['ini_url'],
'transponders_count': len(data['ini_data']['dvb']),
'transponders': data['ini_data']['dvb'],
'sattype_info': data['ini_data']['sattype'],
'metadata': data['ini_data']['metadata']
}
return satellite_dict

View File

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

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -395,6 +395,18 @@ def show_on_map(modeladmin, request, queryset):
show_on_map.short_description = "Показать выбранные на карте" show_on_map.short_description = "Показать выбранные на карте"
def show_selected_on_map(modeladmin, request, queryset):
# Получаем список ID выбранных объектов
selected_ids = queryset.values_list('id', flat=True)
# Формируем строку вида "1,2,3"
ids_str = ','.join(str(pk) for pk in selected_ids)
# Перенаправляем на view, который будет отображать карту с выбранными объектами
return redirect(reverse('show_selected_objects_map') + f'?ids={ids_str}')
show_selected_on_map.short_description = "Показать выбранные объекты на карте"
show_selected_on_map.icon = 'map'
class ParameterObjItemInline(admin.StackedInline): class ParameterObjItemInline(admin.StackedInline):
model = ObjItem.parameters_obj.through model = ObjItem.parameters_obj.through
extra = 0 extra = 0
@@ -443,7 +455,7 @@ class ObjectAdmin(admin.ModelAdmin):
ordering = ("name",) ordering = ("name",)
inlines = [ParameterObjItemInline, GeoInline] inlines = [ParameterObjItemInline, GeoInline]
actions = [show_on_map] actions = [show_on_map, show_selected_on_map]
readonly_fields = ('created_at', 'created_by', 'updated_at', 'updated_by') readonly_fields = ('created_at', 'created_by', 'updated_at', 'updated_by')
def get_queryset(self, request): def get_queryset(self, request):

View File

@@ -123,7 +123,7 @@
<!-- ВЧ загрузки --> <!-- ВЧ загрузки -->
<div class="form-section"> <div class="form-section">
<div class="form-section-header d-flex justify-content-between align-items-center"> <div class="form-section-header d-flex justify-content-between align-items-center">
<h4>ВЧ загрузки</h4> <h4>ВЧ загрузка</h4>
{% if not parameter_forms.forms.0.instance.pk %} {% if not parameter_forms.forms.0.instance.pk %}
<button type="button" class="btn btn-sm btn-outline-primary" id="add-parameter">Добавить ВЧ загрузку</button> <button type="button" class="btn btn-sm btn-outline-primary" id="add-parameter">Добавить ВЧ загрузку</button>
{% endif %} {% endif %}
@@ -131,9 +131,8 @@
<div id="parameters-container"> <div id="parameters-container">
{% for param_form in parameter_forms %} {% for param_form in parameter_forms %}
<div class="dynamic-form" data-parameter-index="{{ forloop.counter0 }}"> {% comment %} <div class="dynamic-form" data-parameter-index="{{ forloop.counter0 }}"> {% endcomment %}
<div class="dynamic-form-header"> <div class="dynamic-form-header">
<h5>ВЧ загрузка #{{ forloop.counter }}</h5>
{% if parameter_forms.forms|length > 1 %} {% if parameter_forms.forms|length > 1 %}
<button type="button" class="btn btn-sm btn-outline-danger remove-parameter">Удалить</button> <button type="button" class="btn btn-sm btn-outline-danger remove-parameter">Удалить</button>
{% endif %} {% endif %}
@@ -190,7 +189,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> {% comment %} </div> {% endcomment %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@@ -365,13 +364,14 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<div class="d-flex justify-content-end mt-4"> <div class="d-flex justify-content-end mt-4">
<button type="submit" class="btn btn-primary btn-action">Сохранить</button> <button type="submit" class="btn btn-primary btn-action">Сохранить</button>
{% if object %} {% if object %}
<a href="{% url 'objitem_delete' object.id %}" class="btn btn-danger btn-action">Удалить</a> <a href="{% url 'objitem_delete' object.id %}" class="btn btn-danger btn-action">Удалить</a>
{% endif %} {% endif %}
</div> </div>
{% endif %}
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,7 +1,17 @@
{% extends 'mainapp/base.html' %} {% extends 'mainapp/base.html' %}
{% block title %}Список объектов{% endblock %} {% block title %}Список объектов{% endblock %}
{% block extra_css %}
<style>
.table-responsive table {
user-select: none;
}
.table-responsive tr.selected {
background-color: #d4edff;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid px-3"> <div class="container-fluid px-3">
<div class="row mb-3"> <div class="row mb-3">
@@ -27,14 +37,19 @@
<!-- Action buttons bar --> <!-- Action buttons bar -->
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="button" class="btn btn-success btn-sm" title="Добавить"> {% comment %} <button type="button" class="btn btn-success btn-sm" title="Добавить">
<i class="bi bi-plus-circle"></i> Добавить <i class="bi bi-plus-circle"></i> Добавить
</button> </button>
<button type="button" class="btn btn-info btn-sm" title="Изменить"> <button type="button" class="btn btn-info btn-sm" title="Изменить">
<i class="bi bi-pencil"></i> Изменить <i class="bi bi-pencil"></i> Изменить
</button> </button> {% endcomment %}
<button type="button" class="btn btn-danger btn-sm" title="Удалить"> {% if user.customuser.role == 'admin' or user.customuser.role == 'moderator' %}
<i class="bi bi-trash"></i> Удалить <button type="button" class="btn btn-danger btn-sm" title="Удалить" onclick="deleteSelectedObjects()">
<i class="bi bi-trash"></i> Удалить
</button>
{% endif %}
<button type="button" class="btn btn-outline-primary btn-sm" title="Показать на карте" onclick="showSelectedOnMap()">
<i class="bi bi-map"></i> Карта
</button> </button>
</div> </div>
@@ -114,52 +129,57 @@
</li> </li>
<li> <li>
<label class="dropdown-item"> <label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="10" checked onchange="toggleColumn(this)"> Геолокация <input type="checkbox" class="column-toggle" data-column="10" checked onchange="toggleColumn(this)"> Местоположение
</label> </label>
</li> </li>
<li> <li>
<label class="dropdown-item"> <label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="11" checked onchange="toggleColumn(this)"> Кубсат <input type="checkbox" class="column-toggle" data-column="11" checked onchange="toggleColumn(this)"> Геолокация
</label> </label>
</li> </li>
<li> <li>
<label class="dropdown-item"> <label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="12" checked onchange="toggleColumn(this)"> Опер. отд <input type="checkbox" class="column-toggle" data-column="12" checked onchange="toggleColumn(this)"> Кубсат
</label> </label>
</li> </li>
<li> <li>
<label class="dropdown-item"> <label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="13" checked onchange="toggleColumn(this)"> Гео-куб, км <input type="checkbox" class="column-toggle" data-column="13" checked onchange="toggleColumn(this)"> Опер. отд
</label> </label>
</li> </li>
<li> <li>
<label class="dropdown-item"> <label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="14" checked onchange="toggleColumn(this)"> Гео-опер, км <input type="checkbox" class="column-toggle" data-column="14" checked onchange="toggleColumn(this)"> Гео-куб, км
</label> </label>
</li> </li>
<li> <li>
<label class="dropdown-item"> <label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="15" checked onchange="toggleColumn(this)"> Куб-опер, км <input type="checkbox" class="column-toggle" data-column="15" checked onchange="toggleColumn(this)"> Гео-опер, км
</label> </label>
</li> </li>
<li> <li>
<label class="dropdown-item"> <label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="16" checked onchange="toggleColumn(this)"> Обновлено <input type="checkbox" class="column-toggle" data-column="16" checked onchange="toggleColumn(this)"> Куб-опер, км
</label> </label>
</li> </li>
<li> <li>
<label class="dropdown-item"> <label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="17" checked onchange="toggleColumn(this)"> Кем (обновление) <input type="checkbox" class="column-toggle" data-column="17" checked onchange="toggleColumn(this)"> Обновлено
</label> </label>
</li> </li>
<li> <li>
<label class="dropdown-item"> <label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="18" checked onchange="toggleColumn(this)"> Создано <input type="checkbox" class="column-toggle" data-column="18" checked onchange="toggleColumn(this)"> Кем (обновление)
</label> </label>
</li> </li>
<li> <li>
<label class="dropdown-item"> <label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="19" checked onchange="toggleColumn(this)"> Кем (создание) <input type="checkbox" class="column-toggle" data-column="19" checked onchange="toggleColumn(this)"> Создано
</label>
</li>
<li>
<label class="dropdown-item">
<input type="checkbox" class="column-toggle" data-column="20" checked onchange="toggleColumn(this)"> Кем (создание)
</label> </label>
</li> </li>
</ul> </ul>
@@ -230,7 +250,7 @@
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('satellite_id', false)">Снять</button>
</div> </div>
<select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="4"> <select name="satellite_id" class="form-select form-select-sm mb-2" multiple size="6">
{% for satellite in satellites %} {% for satellite in satellites %}
<option value="{{ satellite.id }}" <option value="{{ satellite.id }}"
{% if satellite.id in selected_satellites %}selected{% endif %}> {% if satellite.id in selected_satellites %}selected{% endif %}>
@@ -268,8 +288,6 @@
<input type="number" step="0.001" name="bod_max" class="form-control form-control-sm" placeholder="До" value="{{ bod_max|default:'' }}"> <input type="number" step="0.001" name="bod_max" class="form-control form-control-sm" placeholder="До" value="{{ bod_max|default:'' }}">
</div> </div>
<!-- Removed old search input as it's now in the toolbar -->
<!-- Modulation Filter --> <!-- Modulation Filter -->
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Модуляция:</label> <label class="form-label">Модуляция:</label>
@@ -277,7 +295,7 @@
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', true)">Выбрать</button> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', true)">Выбрать</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAllOptions('modulation', false)">Снять</button>
</div> </div>
<select name="modulation" class="form-select form-select-sm mb-2" multiple size="4"> <select name="modulation" class="form-select form-select-sm mb-2" multiple size="6">
{% for mod in modulations %} {% for mod in modulations %}
<option value="{{ mod.id }}" <option value="{{ mod.id }}"
{% if mod.id in selected_modulations %}selected{% endif %}> {% if mod.id in selected_modulations %}selected{% endif %}>
@@ -431,7 +449,7 @@
{% if sort == 'geo_timestamp' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-geo_timestamp' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %} {% if sort == 'geo_timestamp' %} <i class="bi bi-sort-up ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% elif sort == '-geo_timestamp' %} <i class="bi bi-sort-down ms-1"></i> <a href="?{% for key, value in request.GET.items %}{% if key != 'page' and key != 'sort' %}{{ key }}={{ value }}&{% endif %}{% endfor %}" class="text-white ms-1"><i class="bi bi-x-lg"></i></a> {% else %} <i class="bi bi-arrow-down-up ms-1"></i> {% endif %}
</a> </a>
</th> </th>
<th scope="col">Местоположение</th>
<!-- Столбец "Геолокация" - без сортировки --> <!-- Столбец "Геолокация" - без сортировки -->
<th scope="col">Геолокация</th> <th scope="col">Геолокация</th>
@@ -488,6 +506,7 @@
<td>{{ item.modulation }}</td> <td>{{ item.modulation }}</td>
<td>{{ item.snr }}</td> <td>{{ item.snr }}</td>
<td>{{ item.geo_timestamp|date:"d.m.Y H:i" }}</td> <td>{{ item.geo_timestamp|date:"d.m.Y H:i" }}</td>
<td>{{ item.geo_location}}</td>
<td>{{ item.geo_coords }}</td> <td>{{ item.geo_coords }}</td>
<td>{{ item.kupsat_coords }}</td> <td>{{ item.kupsat_coords }}</td>
<td>{{ item.valid_coords }}</td> <td>{{ item.valid_coords }}</td>
@@ -521,6 +540,144 @@
<script> <script>
let lastCheckedIndex = null;
function updateRowHighlight(checkbox) {
const row = checkbox.closest('tr');
if (checkbox.checked) {
row.classList.add('selected');
} else {
row.classList.remove('selected');
}
}
function handleCheckboxClick(e) {
if (e.shiftKey && lastCheckedIndex !== null) {
const checkboxes = document.querySelectorAll('.item-checkbox');
const currentIndex = Array.from(checkboxes).indexOf(e.target);
const startIndex = Math.min(lastCheckedIndex, currentIndex);
const endIndex = Math.max(lastCheckedIndex, currentIndex);
for (let i = startIndex; i <= endIndex; i++) {
checkboxes[i].checked = e.target.checked;
updateRowHighlight(checkboxes[i]);
}
} else {
updateRowHighlight(e.target);
}
lastCheckedIndex = Array.from(document.querySelectorAll('.item-checkbox')).indexOf(e.target);
}
// Function to show selected objects on map
function showSelectedOnMap() {
// Get all checked checkboxes
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один объект для отображения на карте');
return;
}
// Extract IDs from checked checkboxes
const selectedIds = [];
checkedCheckboxes.forEach(checkbox => {
selectedIds.push(checkbox.value);
});
// Redirect to the map view with selected IDs as query parameter
const url = '{% url "show_selected_objects_map" %}' + '?ids=' + selectedIds.join(',');
window.open(url, '_blank'); // Open in a new tab
}
function clearSelections() {
const itemCheckboxes = document.querySelectorAll('.item-checkbox');
itemCheckboxes.forEach(checkbox => {
checkbox.checked = false;
});
// Also uncheck the select-all checkbox if it exists
const selectAllCheckbox = document.getElementById('select-all');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
// Remove selected class from rows
const selectedRows = document.querySelectorAll('tr.selected');
selectedRows.forEach(row => {
row.classList.remove('selected');
});
}
// Function to delete selected objects
function deleteSelectedObjects() {
// Get all checked checkboxes
const checkedCheckboxes = document.querySelectorAll('.item-checkbox:checked');
if (checkedCheckboxes.length === 0) {
alert('Пожалуйста, выберите хотя бы один объект для удаления');
return;
}
// Confirm deletion with user
if (!confirm(`Вы уверены, что хотите удалить ${checkedCheckboxes.length} объект(ов)?`)) {
return;
}
// Extract IDs from checked checkboxes
const selectedIds = [];
checkedCheckboxes.forEach(checkbox => {
selectedIds.push(checkbox.value);
});
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
const csrftoken = getCookie('csrftoken');
// Prepare request headers
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
};
// Add CSRF token to headers only if it exists
if (csrftoken) {
headers['X-CSRFToken'] = csrftoken;
}
// Send AJAX request to delete selected objects
fetch('{% url "delete_selected_objects" %}', {
method: 'POST',
headers: headers,
body: 'ids=' + selectedIds.join(',')
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
clearSelections();
location.reload();
} else {
alert('Ошибка: ' + (data.error || 'Неизвестная ошибка'));
}
})
.catch(error => {
console.error('Error:', error);
alert('Произошла ошибка при удалении объектов');
});
}
// Остальной ваш JavaScript код остается без изменений
function toggleColumn(checkbox) { function toggleColumn(checkbox) {
const columnIndex = parseInt(checkbox.getAttribute('data-column')); const columnIndex = parseInt(checkbox.getAttribute('data-column'));
const table = document.querySelector('.table'); const table = document.querySelector('.table');
@@ -536,7 +693,6 @@ function toggleColumn(checkbox) {
}); });
} }
} }
function toggleAllColumns(selectAllCheckbox) { function toggleAllColumns(selectAllCheckbox) {
const columnCheckboxes = document.querySelectorAll('.column-toggle'); const columnCheckboxes = document.querySelectorAll('.column-toggle');
columnCheckboxes.forEach(checkbox => { columnCheckboxes.forEach(checkbox => {
@@ -545,6 +701,7 @@ function toggleAllColumns(selectAllCheckbox) {
}); });
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const selectAllCheckbox = document.getElementById('select-all'); const selectAllCheckbox = document.getElementById('select-all');
const itemCheckboxes = document.querySelectorAll('.item-checkbox'); const itemCheckboxes = document.querySelectorAll('.item-checkbox');
@@ -562,24 +719,13 @@ document.addEventListener('DOMContentLoaded', function() {
selectAllCheckbox.checked = allChecked; selectAllCheckbox.checked = allChecked;
}); });
}); });
}
// Handle multiple selection for modulations and polarizations // Добавляем обработчик для выбора диапазона
const modulationSelect = document.querySelector('select[name="modulation"]'); itemCheckboxes.forEach(checkbox => {
const polarizationSelect = document.querySelector('select[name="polarization"]'); checkbox.addEventListener('click', handleCheckboxClick);
// Prevent deselecting all options when Ctrl+click is used
if (modulationSelect) {
modulationSelect.addEventListener('change', function(e) {
document.getElementById('filter-form').submit();
}); });
} }
if (polarizationSelect) {
polarizationSelect.addEventListener('change', function(e) {
document.getElementById('filter-form').submit();
});
}
// Handle kubsat and valid coords checkboxes (mutually exclusive) // Handle kubsat and valid coords checkboxes (mutually exclusive)
// Add a function to handle radio-like behavior for these checkboxes // Add a function to handle radio-like behavior for these checkboxes
@@ -597,7 +743,6 @@ document.addEventListener('DOMContentLoaded', function() {
} else { } else {
// If both are unchecked, no action needed // If both are unchecked, no action needed
} }
document.getElementById('filter-form').submit();
}); });
}); });
} }
@@ -612,15 +757,9 @@ document.addEventListener('DOMContentLoaded', function() {
for (let i = 0; i < selectElement.options.length; i++) { for (let i = 0; i < selectElement.options.length; i++) {
selectElement.options[i].selected = selectAll; selectElement.options[i].selected = selectAll;
} }
document.getElementById('filter-form').submit();
} }
}; };
// Function to update the page when satellite selection changes
function updateSatelliteSelection() {
document.getElementById('filter-form').submit();
}
// Get all current filter values and return as URL parameters // Get all current filter values and return as URL parameters
function getAllFilterParams() { function getAllFilterParams() {
const form = document.getElementById('filter-form'); const form = document.getElementById('filter-form');
@@ -633,7 +772,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (searchValue.trim() !== '') { if (searchValue.trim() !== '') {
params.set('search', searchValue); params.set('search', searchValue);
} else { } else {
// Remove search parameter if empty
params.delete('search'); params.delete('search');
} }
@@ -648,9 +786,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Function to clear search // Function to clear search
window.clearSearch = function() { window.clearSearch = function() {
// Clear only the search input in the toolbar
document.getElementById('toolbar-search').value = ''; document.getElementById('toolbar-search').value = '';
// Submit the form to update the results
const filterParams = getAllFilterParams(); const filterParams = getAllFilterParams();
window.location.search = filterParams; window.location.search = filterParams;
}; };
@@ -665,13 +801,12 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// Add event listener to satellite select for immediate update //const satelliteSelect = document.querySelector('select[name="satellite_id"]');
const satelliteSelect = document.querySelector('select[name="satellite_id"]'); //if (satelliteSelect) {
if (satelliteSelect) { //satelliteSelect.addEventListener('change', function() {
satelliteSelect.addEventListener('change', function() { // updateSatelliteSelection();
updateSatelliteSelection(); // });
}); //}
}
// Function to update items per page // Function to update items per page
window.updateItemsPerPage = function() { window.updateItemsPerPage = function() {
@@ -690,16 +825,14 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize column visibility - hide creation columns by default // Initialize column visibility - hide creation columns by default
function initColumnVisibility() { function initColumnVisibility() {
const creationDateCheckbox = document.querySelector('input[data-column="18"]'); const creationDateCheckbox = document.querySelector('input[data-column="19"]');
const creationUserCheckbox = document.querySelector('input[data-column="19"]'); const creationUserCheckbox = document.querySelector('input[data-column="20"]');
const creationDistanceGOpCheckbox = document.querySelector('input[data-column="14"]'); const creationDistanceGOpCheckbox = document.querySelector('input[data-column="15"]');
const creationDistanceKubOpCheckbox = document.querySelector('input[data-column="15"]'); const creationDistanceKubOpCheckbox = document.querySelector('input[data-column="16"]');
if (creationDistanceGOpCheckbox) { if (creationDistanceGOpCheckbox) {
creationDistanceGOpCheckbox.checked = false; creationDistanceGOpCheckbox.checked = false;
toggleColumn(creationDistanceGOpCheckbox); toggleColumn(creationDistanceGOpCheckbox);
} }
if (creationDistanceKubOpCheckbox) { if (creationDistanceKubOpCheckbox) {
creationDistanceKubOpCheckbox.checked = false; creationDistanceKubOpCheckbox.checked = false;
toggleColumn(creationDistanceKubOpCheckbox); toggleColumn(creationDistanceKubOpCheckbox);

View File

@@ -0,0 +1,106 @@
{% extends "mapsapp/map2d_base.html" %}
{% load static %}
{% block title %}Карта выбранных объектов{% endblock title %}
{% block extra_js %}
<script>
// Цвета для стандартных маркеров (из leaflet-color-markers)
var markerColors = ['red', 'green', 'orange', 'violet', 'grey', 'black', 'blue'];
var getColorIcon = function(color) {
return L.icon({
iconUrl: '{% static "leaflet-markers/img/marker-icon-" %}' + color + '.png',
shadowUrl: '{% static "leaflet-markers/img/marker-shadow.png" %}',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
});
};
var overlays = [];
{% for group in groups %}
var groupIndex = {{ forloop.counter0 }};
var colorName = markerColors[groupIndex % markerColors.length];
var groupIcon = getColorIcon(colorName);
var groupLayer = L.layerGroup();
var subgroup = [];
{% for point_data in group.points %}
var pointName = "{{ group.name|escapejs }}";
var marker = L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}], {
icon: groupIcon
}).bindPopup(pointName + '<br>' + "{{ point_data.frequency|escapejs }}");
groupLayer.addLayer(marker);
subgroup.push({
label: "{{ forloop.counter }} - {{ point_data.frequency }}",
layer: marker
});
{% endfor %}
overlays.push({
label: '{{ group.name|escapejs }}',
selectAllCheckbox: true,
children: subgroup,
layer: groupLayer
});
{% endfor %}
// Create the layer control with a custom container that includes a select all checkbox
var layerControl = L.control.layers.tree(baseLayers, overlays, {
collapsed: false,
autoZIndex: true
});
// Add the layer control to the map
layerControl.addTo(map);
// Calculate map bounds to fit all markers
{% if groups %}
var groupBounds = L.featureGroup([]);
{% for group in groups %}
{% for point_data in group.points %}
groupBounds.addLayer(L.marker([{{ point_data.point.1|safe }}, {{ point_data.point.0|safe }}]));
{% endfor %}
{% endfor %}
map.fitBounds(groupBounds.getBounds().pad(0.1)); // Add some padding
{% else %}
map.setView([55.75, 37.62], 5); // Default view if no markers
{% endif %}
// Add a "Select All" checkbox functionality for all overlays
setTimeout(function() {
// Create a custom "select all" checkbox
var selectAllContainer = document.createElement('div');
selectAllContainer.className = 'leaflet-control-layers-select-all';
selectAllContainer.style.padding = '5px';
selectAllContainer.style.borderBottom = '1px solid #ccc';
selectAllContainer.style.marginBottom = '5px';
selectAllContainer.innerHTML = '<label><input type="checkbox" id="select-all-overlays" checked> Показать все точки</label>';
// Insert the checkbox at the top of the layer control
var layerControlContainer = document.querySelector('.leaflet-control-layers-list');
if (layerControlContainer) {
layerControlContainer.insertBefore(selectAllContainer, layerControlContainer.firstChild);
}
// Add event listener to the "select all" checkbox
document.getElementById('select-all-overlays').addEventListener('change', function() {
var isChecked = this.checked;
// Iterate through all overlays and toggle visibility
for (var i = 0; i < overlays.length; i++) {
if (isChecked) {
map.addLayer(overlays[i].layer);
} else {
map.removeLayer(overlays[i].layer);
}
}
});
}, 500); // Slight delay to ensure the tree control has been rendered
</script>
{% endblock extra_js %}

View File

@@ -14,6 +14,8 @@ urlpatterns = [
path('transponders', views.AddTranspondersView.as_view(), name='add_trans'), path('transponders', views.AddTranspondersView.as_view(), name='add_trans'),
path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'), path('csv-data', views.LoadCsvDataView.as_view(), name='load_csv_data'),
path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'), path('map-points/', views.ShowMapView.as_view(), name='admin_show_map'),
path('show-selected-objects-map/', views.ShowSelectedObjectsMapView.as_view(), name='show_selected_objects_map'),
path('delete-selected-objects/', views.DeleteSelectedObjectsView.as_view(), name='delete_selected_objects'),
path('cluster/', views.ClusterTestView.as_view(), name='cluster'), path('cluster/', views.ClusterTestView.as_view(), name='cluster'),
path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'), path('vch-upload/', views.UploadVchLoadView.as_view(), name='vch_load'),
path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'), path('vch-link/', views.LinkVchSigmaView.as_view(), name='link_vch_sigma'),

View File

@@ -228,6 +228,52 @@ class ShowMapView(UserPassesTestMixin, View):
return render(request, 'admin/map_custom.html', context) return render(request, 'admin/map_custom.html', context)
class ShowSelectedObjectsMapView(LoginRequiredMixin, View):
def get(self, request):
ids = request.GET.get('ids', '')
points = []
if ids:
id_list = [int(x) for x in ids.split(',') if x.isdigit()]
locations = ObjItem.objects.filter(id__in=id_list).prefetch_related(
'parameters_obj__id_satellite',
'parameters_obj__polarization',
'parameters_obj__modulation',
'parameters_obj__standard',
'geo_obj'
)
for obj in locations:
param = obj.parameters_obj.get()
points.append({
'name': f"{obj.name}",
'freq': f"{param.frequency} [{param.freq_range}] МГц",
'point': (obj.geo_obj.coords.x, obj.geo_obj.coords.y)
})
else:
return redirect('objitem_list')
# Group points by object name
from collections import defaultdict
grouped = defaultdict(list)
for p in points:
grouped[p["name"]].append({
'point': p["point"],
'frequency': p["freq"]
})
groups = [
{
"name": name,
"points": coords_list
}
for name, coords_list in grouped.items()
]
context = {
'groups': groups,
}
return render(request, 'mainapp/objitem_map.html', context)
class ClusterTestView(LoginRequiredMixin, View): class ClusterTestView(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
objs = ObjItem.objects.filter(name__icontains="! Astra 4A 12654,040 [1,962] МГц H") objs = ObjItem.objects.filter(name__icontains="! Astra 4A 12654,040 [1,962] МГц H")
@@ -316,6 +362,28 @@ class ProcessKubsatView(LoginRequiredMixin, FormView):
messages.error(self.request, "Форма заполнена некорректно.") messages.error(self.request, "Форма заполнена некорректно.")
return super().form_invalid(form) return super().form_invalid(form)
class DeleteSelectedObjectsView(LoginRequiredMixin, View):
def post(self, request):
if request.user.customuser.role not in ['admin', 'moderator']:
return JsonResponse({'error': 'У вас нет прав для удаления объектов'}, status=403)
ids = request.POST.get('ids', '')
if not ids:
return JsonResponse({'error': 'Нет ID для удаления'}, status=400)
try:
id_list = [int(x) for x in ids.split(',') if x.isdigit()]
deleted_count, _ = ObjItem.objects.filter(id__in=id_list).delete()
return JsonResponse({
'success': True,
'message': 'Объект успешно удалён',
# 'deleted_count': deleted_count
})
except Exception as e:
return JsonResponse({'error': f'Ошибка при удалении: {str(e)}'}, status=500)
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
class ObjItemListView(LoginRequiredMixin, View): class ObjItemListView(LoginRequiredMixin, View):
@@ -520,6 +588,8 @@ class ObjItemListView(LoginRequiredMixin, View):
param = param_list[0] param = param_list[0]
geo_coords = "-" geo_coords = "-"
geo_timestamp = "-"
geo_location = "-"
kupsat_coords = "-" kupsat_coords = "-"
valid_coords = "-" valid_coords = "-"
distance_geo_kup = "-" distance_geo_kup = "-"
@@ -528,6 +598,8 @@ class ObjItemListView(LoginRequiredMixin, View):
if obj.geo_obj: if obj.geo_obj:
geo_timestamp = obj.geo_obj.timestamp geo_timestamp = obj.geo_obj.timestamp
geo_location = obj.geo_obj.location
if obj.geo_obj.coords: if obj.geo_obj.coords:
longitude = obj.geo_obj.coords.coords[0] longitude = obj.geo_obj.coords.coords[0]
latitude = obj.geo_obj.coords.coords[1] latitude = obj.geo_obj.coords.coords[1]
@@ -596,6 +668,7 @@ class ObjItemListView(LoginRequiredMixin, View):
'modulation': modulation_name, 'modulation': modulation_name,
'snr': snr, 'snr': snr,
'geo_timestamp': geo_timestamp, 'geo_timestamp': geo_timestamp,
'geo_location': geo_location,
'geo_coords': geo_coords, 'geo_coords': geo_coords,
'kupsat_coords': kupsat_coords, 'kupsat_coords': kupsat_coords,
'valid_coords': valid_coords, 'valid_coords': valid_coords,

View File

@@ -7,6 +7,7 @@ requires-python = ">=3.13"
dependencies = [ dependencies = [
"aiosqlite>=0.21.0", "aiosqlite>=0.21.0",
"bcrypt>=5.0.0", "bcrypt>=5.0.0",
"beautifulsoup4>=4.14.2",
"django>=5.2.7", "django>=5.2.7",
"django-admin-interface>=0.30.1", "django-admin-interface>=0.30.1",
"django-admin-multiple-choice-list-filter>=0.1.1", "django-admin-multiple-choice-list-filter>=0.1.1",
@@ -32,6 +33,7 @@ dependencies = [
"requests>=2.32.5", "requests>=2.32.5",
"reverse-geocoder>=1.5.1", "reverse-geocoder>=1.5.1",
"scikit-learn>=1.7.2", "scikit-learn>=1.7.2",
"selenium>=4.38.0",
"setuptools>=80.9.0", "setuptools>=80.9.0",
] ]

186
dbapp/uv.lock generated
View File

@@ -23,6 +23,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" }, { url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" },
] ]
[[package]]
name = "attrs"
version = "25.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
]
[[package]] [[package]]
name = "bcrypt" name = "bcrypt"
version = "5.0.0" version = "5.0.0"
@@ -89,6 +98,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" },
] ]
[[package]]
name = "beautifulsoup4"
version = "4.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2025.10.5" version = "2025.10.5"
@@ -98,6 +120,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
] ]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.3" version = "3.4.3"
@@ -200,6 +242,7 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },
{ name = "bcrypt" }, { name = "bcrypt" },
{ name = "beautifulsoup4" },
{ name = "django" }, { name = "django" },
{ name = "django-admin-interface" }, { name = "django-admin-interface" },
{ name = "django-admin-multiple-choice-list-filter" }, { name = "django-admin-multiple-choice-list-filter" },
@@ -225,6 +268,7 @@ dependencies = [
{ name = "requests" }, { name = "requests" },
{ name = "reverse-geocoder" }, { name = "reverse-geocoder" },
{ name = "scikit-learn" }, { name = "scikit-learn" },
{ name = "selenium" },
{ name = "setuptools" }, { name = "setuptools" },
] ]
@@ -232,6 +276,7 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "aiosqlite", specifier = ">=0.21.0" }, { name = "aiosqlite", specifier = ">=0.21.0" },
{ name = "bcrypt", specifier = ">=5.0.0" }, { name = "bcrypt", specifier = ">=5.0.0" },
{ name = "beautifulsoup4", specifier = ">=4.14.2" },
{ name = "django", specifier = ">=5.2.7" }, { name = "django", specifier = ">=5.2.7" },
{ name = "django-admin-interface", specifier = ">=0.30.1" }, { name = "django-admin-interface", specifier = ">=0.30.1" },
{ name = "django-admin-multiple-choice-list-filter", specifier = ">=0.1.1" }, { name = "django-admin-multiple-choice-list-filter", specifier = ">=0.1.1" },
@@ -257,6 +302,7 @@ requires-dist = [
{ name = "requests", specifier = ">=2.32.5" }, { name = "requests", specifier = ">=2.32.5" },
{ name = "reverse-geocoder", specifier = ">=1.5.1" }, { name = "reverse-geocoder", specifier = ">=1.5.1" },
{ name = "scikit-learn", specifier = ">=1.7.2" }, { name = "scikit-learn", specifier = ">=1.7.2" },
{ name = "selenium", specifier = ">=4.38.0" },
{ name = "setuptools", specifier = ">=80.9.0" }, { name = "setuptools", specifier = ">=80.9.0" },
] ]
@@ -505,6 +551,15 @@ 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" }, { 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 = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.10"
@@ -755,6 +810,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
] ]
[[package]]
name = "outcome"
version = "1.3.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" },
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "25.0" version = "25.0"
@@ -871,6 +938,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/90/422ffbbeeb9418c795dae2a768db860401446af0c6768bc061ce22325f58/psycopg-3.2.10-py3-none-any.whl", hash = "sha256:ab5caf09a9ec42e314a21f5216dbcceac528e0e05142e42eea83a3b28b320ac3", size = 206586, upload-time = "2025-09-08T09:07:50.121Z" }, { url = "https://files.pythonhosted.org/packages/4a/90/422ffbbeeb9418c795dae2a768db860401446af0c6768bc061ce22325f58/psycopg-3.2.10-py3-none-any.whl", hash = "sha256:ab5caf09a9ec42e314a21f5216dbcceac528e0e05142e42eea83a3b28b320ac3", size = 206586, upload-time = "2025-09-08T09:07:50.121Z" },
] ]
[[package]]
name = "pycparser"
version = "2.23"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "3.2.5" version = "3.2.5"
@@ -880,6 +956,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
] ]
[[package]]
name = "pysocks"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@@ -1036,6 +1121,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" }, { url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" },
] ]
[[package]]
name = "selenium"
version = "4.38.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "trio" },
{ name = "trio-websocket" },
{ name = "typing-extensions" },
{ name = "urllib3", extra = ["socks"] },
{ name = "websocket-client" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/a0/60a5e7e946420786d57816f64536e21a29f0554706b36f3cba348107024c/selenium-4.38.0.tar.gz", hash = "sha256:c117af6727859d50f622d6d0785b945c5db3e28a45ec12ad85cee2e7cc84fc4c", size = 924101, upload-time = "2025-10-25T02:13:06.752Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/d3/76c8f4a8d99b9f1ebcf9a611b4dd992bf5ee082a6093cfc649af3d10f35b/selenium-4.38.0-py3-none-any.whl", hash = "sha256:ed47563f188130a6fd486b327ca7ba48c5b11fb900e07d6457befdde320e35fd", size = 9694571, upload-time = "2025-10-25T02:13:04.417Z" },
]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "80.9.0" version = "80.9.0"
@@ -1054,6 +1156,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
] ]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
]
[[package]]
name = "soupsieve"
version = "2.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
]
[[package]] [[package]]
name = "sqlparse" name = "sqlparse"
version = "0.5.3" version = "0.5.3"
@@ -1090,6 +1219,37 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
] ]
[[package]]
name = "trio"
version = "0.32.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
{ name = "idna" },
{ name = "outcome" },
{ name = "sniffio" },
{ name = "sortedcontainers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d8/ce/0041ddd9160aac0031bcf5ab786c7640d795c797e67c438e15cfedf815c8/trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b", size = 605323, upload-time = "2025-10-31T07:18:17.466Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/bf/945d527ff706233636c73880b22c7c953f3faeb9d6c7e2e85bfbfd0134a0/trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5", size = 512030, upload-time = "2025-10-31T07:18:15.885Z" },
]
[[package]]
name = "trio-websocket"
version = "0.12.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "outcome" },
{ name = "trio" },
{ name = "wsproto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"
@@ -1116,3 +1276,29 @@ sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
] ]
[package.optional-dependencies]
socks = [
{ name = "pysocks" },
]
[[package]]
name = "websocket-client"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
]
[[package]]
name = "wsproto"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" },
]