Рефакторинг проекта: разделение на модули, улучшенный интерфейс, обработка Excel

This commit is contained in:
2025-08-04 00:31:05 +05:00
parent 059f174dd5
commit a882a81180
22 changed files with 1936 additions and 530 deletions

12
.gitignore vendored
View File

@@ -1,3 +1,13 @@
*.pyc *.pyc
__pycache__/ __pycache__/
kadastr14_temp/ kadastr14_temp/
найтийденные файлы/
results/
.venv/
venv/
build/
*.xml
build_32bit/
dist_32bit/
dist/
testset/

52
BUILD_INSTRUCTIONS.md Normal file
View File

@@ -0,0 +1,52 @@
# Инструкция по созданию 32-битной версии программы
## Текущее состояние
- ✅ Программа успешно работает на 64-битных системах
- ❌ Для 32-битных систем требуется пересборка
## Для создания 32-битной версии:
### Шаг 1: Установка 32-битного Python
1. Скачайте Python 3.12 (32-bit) с https://www.python.org/downloads/
2. При установке выберите "Add to PATH"
3. Убедитесь, что установлен именно 32-битный Python
### Шаг 2: Проверка архитектуры
```bash
python -c "import platform; print(platform.architecture())"
# Должно показать: ('32bit', 'WindowsPE')
```
### Шаг 3: Установка зависимостей
```bash
pip install pyinstaller pandas openpyxl lxml pathlib2
```
### Шаг 4: Сборка программы
```bash
python build_32bit.py
```
## Альтернативный вариант
Если у вас нет доступа к 32-битному Python, можно:
1. **Использовать виртуальную машину** с 32-битной Windows
2. **Попросить кого-то собрать** на 32-битной системе
3. **Использовать Docker** с 32-битным образом Python
## Проверка совместимости
Созданный `KadastrParser.exe` (64-bit) будет работать на:
- ✅ Windows 7/8/10/11 (64-bit)
- ❌ Windows 7/8/10/11 (32-bit)
Для универсальности рекомендуется создать обе версии:
- `KadastrParser_64bit.exe` - для 64-битных систем
- `KadastrParser_32bit.exe` - для 32-битных систем
## Размер программы
- 64-битная версия: ~50-70 MB
- 32-битная версия: ~45-65 MB
## Тестирование
Перед выпуском обязательно протестируйте на целевой системе!

39
KadastrParser.spec Normal file
View File

@@ -0,0 +1,39 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[('services', 'services'), ('models', 'models')],
hiddenimports=['tkinter', 'pandas', 'openpyxl', 'lxml'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='KadastrParser',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='NONE',
)

39
KadastrParser_32bit.spec Normal file
View File

@@ -0,0 +1,39 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[('services', 'services'), ('models', 'models')],
hiddenimports=['tkinter', 'pandas', 'openpyxl', 'lxml', 'pathlib2'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='KadastrParser_32bit',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='NONE',
)

117
VERSIONS_INFO.md Normal file
View File

@@ -0,0 +1,117 @@
# KadastrParser - Парсер кадастровых номеров
## 📦 Доступные версии
### 🖥️ 64-битная версия
- **Файл**: `dist/KadastrParser.exe`
- **Размер**: ~52 MB
- **Совместимость**: Windows 7/8/10/11 (только 64-bit)
- **Python**: 3.12.11 (64-bit)
### 💻 32-битная версия
- **Файл**: `dist_32bit/KadastrParser_32bit.exe`
- **Размер**: ~29 MB
- **Совместимость**: Windows 7/8/10/11 (32-bit и 64-bit)
- **Python**: 3.8.20 (32-bit)
## ✅ Ответ на ваш вопрос
**Да, программа будет запускаться на 32-битной системе!**
Используйте файл `KadastrParser_32bit.exe` из папки `dist_32bit/`.
## 🎯 Рекомендации по выбору версии
### Для 32-битных систем:
- ✅ Используйте `KadastrParser_32bit.exe`
- ✅ Меньший размер файла
- ✅ Гарантированная совместимость
### Для 64-битных систем:
- 🔄 Можно использовать любую версию
- 🚀 `KadastrParser.exe` - более новый Python, быстрее
- 💾 `KadastrParser_32bit.exe` - универсальная
## 🔧 Функциональность
Обе версии имеют **абсолютно одинаковый функционал**:
### ✨ Основные возможности:
- 📁 Поиск XML файлов в директории
- 🔍 Извлечение кадастровых номеров
- 📊 Обработка Excel файлов со списками номеров
- 📋 Создание подробных отчетов
- 🗂️ Организация файлов по папкам
### 📈 Excel интеграция:
-**Найденные файлы** - где обнаружен номер
-**Смежные кадастровые номера** - другие номера из тех же файлов
-**Кадастровый блок** - первые 3 части номера
- ✅ Автоматическое обновление Excel с путями к файлам
### 📁 Структура результатов:
```
results/
├── all_xml_files/ # Все найденные XML файлы
├── matching_cadastral_files/ # Файлы из Excel списка
├── updated_cadastral_list.xlsx # Обновленный Excel файл
└── processing_report.txt # Подробный отчет
```
## 🚀 Как использовать:
1. **Скачайте нужную версию**:
- 32-bit система → `KadastrParser_32bit.exe`
- 64-bit система → `KadastrParser.exe` или `KadastrParser_32bit.exe`
2. **Поместите файл в любую папку**
3. **Запустите двойным кликом**
4. **Выберите папку с XML файлами**
5. **Опционально: выберите Excel файл**
6. **Нажмите "Начать обработку"**
-**В этот момент автоматически создается папка `results/` в той же папке, где лежит программа**
-**Создаются подпапки для организации файлов**
## ⏰ Когда и где создаются директории:
- 📁 **Сразу при нажатии "Начать обработку"** программа создает папку `results/` **в той же директории, где находится исполняемый файл**:
- `results/` - основная папка результатов
- `results/all_xml_files/` - для всех найденных XML файлов
- `results/matching_cadastral_files/` - для файлов из Excel списка
- 📄 **Во время обработки** добавляются:
- `results/processing_report.txt` - подробный отчет
- `results/updated_cadastral_list.xlsx` - обновленный Excel (если указан)
### 📍 Пример расположения файлов:
```
C:\MyFolder\
├── KadastrParser.exe # Ваша программа
└── results/ # Создается автоматически
├── all_xml_files/
├── matching_cadastral_files/
├── processing_report.txt
└── updated_cadastral_list.xlsx
```
## 💡 Важные особенности:
- 🔄 **Без консоли** - чистый графический интерфейс
- 📦 **Один файл** - не требует установки
- 🛡️ **Безопасно** - только чтение XML и создание результатов
- 🎨 **Интуитивный интерфейс** - простое использование
-**Быстрая обработка** - многопоточность
## 🔍 Поддерживаемые форматы:
- **Кадастровые номера**: `XX:XX:XXXXXXX:XXX`
- **XML файлы**: все варианты из ЕГРН
- **Excel файлы**: `.xlsx` и `.xls`
---
**Итог**: Для 32-битной системы используйте `KadastrParser_32bit.exe` - он гарантированно будет работать!

72
build_32bit.py Normal file
View File

@@ -0,0 +1,72 @@
"""
Скрипт для создания 32-битной версии программы
ВАЖНО: Этот скрипт нужно запускать на 32-битном Python!
"""
import PyInstaller.__main__
import sys
import platform
def check_architecture():
"""Проверяет архитектуру Python"""
arch = platform.architecture()[0]
machine = platform.machine()
print(f"🔍 Текущая архитектура Python: {arch}")
print(f"🖥️ Тип машины: {machine}")
if arch == '32bit':
print("✅ Python 32-bit - подходит для создания 32-битной программы")
return True
else:
print("❌ Python 64-bit - НЕ подходит для создания 32-битной программы")
print("💡 Для создания 32-битной версии нужен 32-битный Python")
return False
def build_32bit_executable():
"""Создает 32-битный исполняемый файл"""
if not check_architecture():
print("\n⚠️ ВНИМАНИЕ: Для создания 32-битной программы:")
print("1. Скачайте 32-битный Python с https://www.python.org/downloads/")
print("2. Установите зависимости: pip install pyinstaller pandas openpyxl lxml")
print("3. Запустите этот скрипт снова")
return False
# Параметры для PyInstaller (32-бит)
args = [
'main.py',
'--onefile',
'--windowed',
'--name=KadastrParser_32bit',
'--icon=NONE',
'--add-data=services;services',
'--add-data=models;models',
'--hidden-import=tkinter',
'--hidden-import=pandas',
'--hidden-import=openpyxl',
'--hidden-import=lxml',
'--clean',
'--noconfirm',
'--target-architecture=x86', # Явно указываем 32-бит
]
print("\n🚀 Начинаем создание 32-битного исполняемого файла...")
print("📋 Параметры сборки:")
for arg in args:
print(f" {arg}")
print()
try:
# Запускаем PyInstaller
PyInstaller.__main__.run(args)
print("✅ 32-битная сборка завершена!")
print("📁 Исполняемый файл: dist/KadastrParser_32bit.exe")
return True
except Exception as e:
print(f"❌ Ошибка при сборке: {e}")
return False
if __name__ == "__main__":
print("🔧 Создание 32-битной версии программы")
print("=" * 50)
build_32bit_executable()

158
build_32bit_final.py Normal file
View File

@@ -0,0 +1,158 @@
"""
Скрипт для создания 32-битной версии программы KadastrParser
Использует Python 3.8 (32-bit)
"""
import subprocess
import sys
import os
from pathlib import Path
def build_32bit_version():
"""Создает 32-битную версию программы"""
print("🔧 Создание 32-битной версии KadastrParser")
print("=" * 50)
# Проверяем, что мы в правильной директории
if not os.path.exists("main.py"):
print("❌ Файл main.py не найден!")
print(" Убедитесь, что скрипт запускается из директории с исходным кодом")
return False
# Параметры для PyInstaller (32-bit)
pyinstaller_args = [
"py", "-3.8-32", "-m", "PyInstaller",
"main.py",
"--onefile",
"--windowed",
"--name=KadastrParser_32bit",
"--icon=NONE",
"--add-data=services;services",
"--add-data=models;models",
"--hidden-import=tkinter",
"--hidden-import=pandas",
"--hidden-import=openpyxl",
"--hidden-import=lxml",
"--hidden-import=pathlib2",
"--clean",
"--noconfirm",
"--distpath=dist_32bit", # Отдельная папка для 32-битной версии
"--workpath=build_32bit",
"--specpath=."
]
print("📋 Параметры сборки:")
for arg in pyinstaller_args[4:]: # Пропускаем "py -3.8-32 -m PyInstaller"
print(f" {arg}")
print()
print("🚀 Запуск PyInstaller...")
try:
# Запускаем PyInstaller
result = subprocess.run(pyinstaller_args, check=True, capture_output=True, text=True)
print("✅ 32-битная сборка завершена успешно!")
# Проверяем результат
exe_path = Path("dist_32bit") / "KadastrParser_32bit.exe"
if exe_path.exists():
size_mb = exe_path.stat().st_size / (1024 * 1024)
print(f"📁 Исполняемый файл: {exe_path}")
print(f"📏 Размер файла: {size_mb:.1f} MB")
# Проверяем архитектуру созданного файла
check_exe_architecture(exe_path)
else:
print("❌ Исполняемый файл не найден!")
return False
return True
except subprocess.CalledProcessError as e:
print(f"❌ Ошибка при сборке:")
print(f" Код выхода: {e.returncode}")
if e.stdout:
print(f" Вывод: {e.stdout}")
if e.stderr:
print(f" Ошибки: {e.stderr}")
return False
except Exception as e:
print(f"❌ Неожиданная ошибка: {e}")
return False
def check_exe_architecture(exe_path):
"""Проверяет архитектуру исполняемого файла"""
try:
# Используем file command или dumpbin если доступны
result = subprocess.run(
["file", str(exe_path)],
capture_output=True,
text=True,
errors='ignore'
)
if result.returncode == 0:
output = result.stdout.lower()
if "pe32" in output and "pe32+" not in output:
print("✅ Архитектура: 32-bit (PE32)")
elif "pe32+" in output:
print("⚠️ Архитектура: 64-bit (PE32+)")
else:
print(f" Архитектура: {output.strip()}")
else:
print(" Не удалось определить архитектуру (file command недоступен)")
except FileNotFoundError:
print(" Не удалось определить архитектуру (file command не найден)")
except Exception as e:
print(f" Ошибка при проверке архитектуры: {e}")
def create_readme():
"""Создает README для 32-битной версии"""
readme_content = """# KadastrParser 32-bit версия
## Системные требования
- Windows 7/8/10/11 (32-bit или 64-bit)
- Минимум 512 MB оперативной памяти
- 100 MB свободного места на диске
## Установка
1. Скачайте KadastrParser_32bit.exe
2. Поместите файл в любую папку
3. Запустите двойным кликом
## Использование
1. Запустите программу
2. Выберите папку с XML файлами
3. (Опционально) Выберите Excel файл со списком кадастровых номеров
4. Нажмите "Начать обработку"
## Результаты
Программа создает папку "results" рядом с exe файлом:
- all_xml_files/ - все найденные XML файлы
- matching_cadastral_files/ - файлы из Excel списка
- updated_cadastral_list.xlsx - обновленный Excel файл
- processing_report.txt - подробный отчет
## Поддержка
Программа поддерживает кадастровые номера в формате: XX:XX:XXXXXXX:XXX
"""
readme_path = Path("dist_32bit") / "README.txt"
readme_path.parent.mkdir(exist_ok=True)
with open(readme_path, 'w', encoding='utf-8') as f:
f.write(readme_content)
print(f"📝 Создан файл README: {readme_path}")
if __name__ == "__main__":
success = build_32bit_version()
if success:
create_readme()
print("\n🎉 32-битная версия готова к использованию!")
print("📦 Файлы находятся в папке 'dist_32bit'")
else:
print("\n💥 Сборка не удалась!")
sys.exit(1)

41
build_script.py Normal file
View File

@@ -0,0 +1,41 @@
"""
Скрипт для создания исполняемого файла программы
"""
import PyInstaller.__main__
import sys
import os
def build_executable():
"""Создает исполняемый файл без консоли"""
# Параметры для PyInstaller
args = [
'main.py', # Главный файл
'--onefile', # Один исполняемый файл
'--windowed', # Без консоли (для GUI приложений)
'--name=KadastrParser', # Имя исполняемого файла
'--icon=NONE', # Без иконки
'--add-data=services;services', # Включаем папку services
'--add-data=models;models', # Включаем папку models
'--hidden-import=tkinter', # Явно включаем tkinter
'--hidden-import=pandas', # Явно включаем pandas
'--hidden-import=openpyxl', # Явно включаем openpyxl
'--hidden-import=lxml', # Явно включаем lxml
'--clean', # Очищаем кэш
'--noconfirm', # Не запрашиваем подтверждение
]
print("🚀 Начинаем создание исполняемого файла...")
print("📋 Параметры сборки:")
for arg in args:
print(f" {arg}")
print()
# Запускаем PyInstaller
PyInstaller.__main__.run(args)
print("✅ Сборка завершена!")
print("📁 Исполняемый файл находится в папке 'dist'")
if __name__ == "__main__":
build_executable()

45
cadastral_record.py Normal file
View File

@@ -0,0 +1,45 @@
"""
Модуль для работы с кадастровыми записями.
Содержит класс CadastralRecord для хранения информации о кадастровых номерах.
"""
class CadastralRecord:
"""Класс для хранения информации о кадастровом номере и связанных с ним данных."""
def __init__(self, cadastral_number, source_xml_info, temp_file_path=None):
self.cadastral_number = cadastral_number # Основной кадастровый номер
self.source_xml_info = source_xml_info # Ссылка на XMLFileInfo где найден
self.temp_file_path = temp_file_path # Путь к файлу во временной папке
self.quarter_number = self._extract_quarter_number() # Номер квартала
self.related_numbers = [] # Смежные кадастровые номера из того же файла
def _extract_quarter_number(self):
"""Извлекает номер квартала из кадастрового номера."""
# Квартал - это первые три части номера (NN:NN:NNNNNNN)
parts = self.cadastral_number.split(':')
if len(parts) >= 3:
return ':'.join(parts[:3])
return None
def set_related_numbers(self, all_numbers_from_file):
"""Устанавливает список смежных кадастровых номеров из того же файла."""
# Убираем сам номер из списка смежных
self.related_numbers = [num for num in all_numbers_from_file if num != self.cadastral_number]
def get_object_type(self):
"""Определяет тип объекта по кадастровому номеру."""
parts = self.cadastral_number.split(':')
if len(parts) == 4:
# Полный кадастровый номер объекта недвижимости
return "Объект недвижимости"
elif len(parts) == 3:
# Номер квартала
return "Квартал"
else:
return "Неизвестный тип"
def __str__(self):
return f"CadastralRecord({self.cadastral_number}, квартал: {self.quarter_number})"
def __repr__(self):
return self.__str__()

34
demo_structure.py Normal file
View File

@@ -0,0 +1,34 @@
"""
Демонстрация новой структуры папок
"""
from pathlib import Path
def show_folder_structure():
"""Показывает структуру папок которые создает программа"""
print("🗂️ СТРУКТУРА РЕЗУЛЬТАТОВ ПРОГРАММЫ:")
print()
print("📁 results/")
print("├── 📁 all_xml_files/")
print("│ ├── 74_34_0400003_1234_report-example[0].xml")
print("│ ├── 74_34_0400003_5678_report-example[1].xml")
print("│ └── ... (все найденные XML файлы с кадастровыми номерами)")
print("")
print("├── 📁 matching_cadastral_files/")
print("│ ├── 74_34_0400003_1234_report-match[0].xml")
print("│ └── ... (только файлы с номерами из Excel списка)")
print("")
print("├── 📊 updated_cadastral_list.xlsx")
print("│ (Excel с добавленной колонкой путей к файлам)")
print("")
print("└── 📋 processing_report.txt")
print(" (подробный отчет о работе)")
print()
print("✨ ПРЕИМУЩЕСТВА:")
print("Все файлы структурированы и легко найти")
print("• Отдельная папка для файлов из Excel списка")
print("• Имена файлов содержат кадастровые номера")
print("• Полная отчетность о проделанной работе")
if __name__ == "__main__":
show_folder_structure()

241
excel_processor.py Normal file
View File

@@ -0,0 +1,241 @@
"""
Модуль для работы с Excel файлами.
Обрабатывает список кадастровых номеров и заполняет информацию о них.
"""
import pandas as pd
from pathlib import Path
from typing import List, Dict, Optional
from collections import defaultdict
class ExcelProcessor:
"""Класс для работы с Excel файлами списков кадастровых номеров."""
def __init__(self, excel_file_path: str):
self.excel_file_path = Path(excel_file_path)
self.df = None
self.cadastral_column = None
def load_excel(self, sheet_name: Optional[str] = None, cadastral_column: Optional[str] = None):
"""Загружает Excel файл и определяет колонку с кадастровыми номерами."""
try:
# Загружаем Excel файл
if sheet_name:
self.df = pd.read_excel(self.excel_file_path, sheet_name=sheet_name)
else:
# Берем первый лист
self.df = pd.read_excel(self.excel_file_path)
print(f"✅ Загружен Excel файл: {self.excel_file_path}")
print(f"📊 Размер таблицы: {len(self.df)} строк, {len(self.df.columns)} колонок")
print(f"📋 Колонки: {list(self.df.columns)}")
# Определяем колонку с кадастровыми номерами
if cadastral_column:
self.cadastral_column = cadastral_column
else:
self.cadastral_column = self._find_cadastral_column()
if self.cadastral_column:
print(f"🎯 Найдена колонка с кадастровыми номерами: '{self.cadastral_column}'")
cadastral_count = self.df[self.cadastral_column].notna().sum()
print(f"📄 Кадастровых номеров в файле: {cadastral_count}")
else:
raise ValueError("Не удалось найти колонку с кадастровыми номерами")
except Exception as e:
print(f"❌ Ошибка при загрузке Excel файла: {e}")
raise
def _find_cadastral_column(self) -> Optional[str]:
"""Автоматически находит колонку с кадастровыми номерами."""
# Возможные названия колонок
possible_names = [
'кадастровый номер', 'кадастровый_номер', 'кн', 'cadastral_number',
'номер', 'cadastral', 'кадастр', 'kad_number'
]
# Ищем по названию колонки
for col in self.df.columns:
col_lower = str(col).lower().strip()
for name in possible_names:
if name in col_lower:
return col
# Ищем по содержимому (ищем колонку где есть номера вида XX:XX:XXXXXX:XXXX)
import re
pattern = r'\d{1,2}:\d{1,2}:\d{6,7}:\d{1,4}'
for col in self.df.columns:
# Проверяем первые несколько непустых значений
sample_values = self.df[col].dropna().head(10)
match_count = 0
for value in sample_values:
if isinstance(value, str) and re.match(pattern, value.strip()):
match_count += 1
# Если больше половины значений похожи на кадастровые номера
if match_count > len(sample_values) * 0.5 and match_count > 0:
return col
return None
def get_cadastral_numbers_from_excel(self) -> List[str]:
"""Возвращает список всех кадастровых номеров из Excel файла."""
if not self.cadastral_column:
return []
# Получаем все непустые кадастровые номера
cadastral_numbers = []
for value in self.df[self.cadastral_column].dropna():
if isinstance(value, str):
clean_value = value.strip()
if clean_value:
cadastral_numbers.append(clean_value)
elif pd.notna(value):
# Если число, преобразуем в строку
cadastral_numbers.append(str(value).strip())
return cadastral_numbers
def create_cadastral_mapping(self, cadastral_records: List) -> Dict[str, Dict]:
"""Создает маппинг кадастровых номеров к их данным."""
mapping = defaultdict(lambda: {
'source_files': set(),
'related_numbers': set(),
'quarter_numbers': set()
})
for record in cadastral_records:
cadastral_num = record.cadastral_number
# Добавляем источник файла
source_file = record.source_xml_info.get_display_name()
mapping[cadastral_num]['source_files'].add(source_file)
# Добавляем связанные номера
for related in record.related_numbers:
mapping[cadastral_num]['related_numbers'].add(related)
# Добавляем квартал
if record.quarter_number:
mapping[cadastral_num]['quarter_numbers'].add(record.quarter_number)
# Преобразуем sets в списки для удобства
for cadastral_num in mapping:
mapping[cadastral_num]['source_files'] = list(mapping[cadastral_num]['source_files'])
mapping[cadastral_num]['related_numbers'] = list(mapping[cadastral_num]['related_numbers'])
mapping[cadastral_num]['quarter_numbers'] = list(mapping[cadastral_num]['quarter_numbers'])
return dict(mapping)
def fill_cadastral_info(self, cadastral_records: List, output_file: Optional[str] = None):
"""Заполняет информацию о кадастровых номерах в Excel файле."""
if self.df is None or not self.cadastral_column:
raise ValueError("Excel файл не загружен или не найдена колонка с кадастровыми номерами")
print(f"\n{'='*80}")
print(f"ЗАПОЛНЕНИЕ ИНФОРМАЦИИ В EXCEL ФАЙЛЕ")
print(f"{'='*80}")
# Создаем маппинг
cadastral_mapping = self.create_cadastral_mapping(cadastral_records)
# Добавляем новые колонки если их нет
if 'Источники файлов' not in self.df.columns:
self.df['Источники файлов'] = ''
if 'Смежные номера' not in self.df.columns:
self.df['Смежные номера'] = ''
if 'Кадастровый блок' not in self.df.columns:
self.df['Кадастровый блок'] = ''
# Статистика
found_count = 0
not_found_count = 0
# Заполняем информацию для каждого кадастрового номера
for index, row in self.df.iterrows():
cadastral_num = str(row[self.cadastral_column]).strip()
if cadastral_num in cadastral_mapping:
data = cadastral_mapping[cadastral_num]
# Заполняем источники файлов
sources = ', '.join(data['source_files'])
self.df.at[index, 'Источники файлов'] = sources
# Заполняем смежные номера
related = ', '.join(data['related_numbers'])
self.df.at[index, 'Смежные номера'] = related
# Заполняем кадастровый блок (квартал)
quarters = ', '.join(data['quarter_numbers'])
self.df.at[index, 'Кадастровый блок'] = quarters
found_count += 1
print(f"{cadastral_num}: найден в {len(data['source_files'])} файлах")
else:
not_found_count += 1
print(f"{cadastral_num}: не найден в XML файлах")
print(f"\n📊 Статистика заполнения:")
print(f" Найдено: {found_count}")
print(f" Не найдено: {not_found_count}")
print(f" Всего: {found_count + not_found_count}")
# Сохраняем результат
if output_file:
output_path = Path(output_file)
else:
# Создаем имя файла с результатами
output_path = self.excel_file_path.parent / f"{self.excel_file_path.stem}_результат.xlsx"
try:
self.df.to_excel(output_path, index=False)
print(f"\n💾 Результат сохранен в: {output_path}")
return output_path
except Exception as e:
print(f"❌ Ошибка при сохранении: {e}")
raise
def print_excel_preview(self, rows: int = 10):
"""Выводит предварительный просмотр Excel файла."""
if self.df is None:
print("Excel файл не загружен")
return
print(f"\n📋 Предварительный просмотр Excel файла (первые {rows} строк):")
print("-" * 80)
# Показываем первые строки
preview_df = self.df.head(rows)
# Ограничиваем ширину колонок для читаемости
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)
pd.set_option('display.max_colwidth', 50)
print(preview_df.to_string(index=False))
if len(self.df) > rows:
print(f"\n... и еще {len(self.df) - rows} строк")
def get_statistics(self):
"""Возвращает статистику по Excel файлу."""
if self.df is None:
return {}
stats = {
'total_rows': len(self.df),
'total_columns': len(self.df.columns),
'columns': list(self.df.columns),
'cadastral_column': self.cadastral_column
}
if self.cadastral_column:
cadastral_numbers = self.get_cadastral_numbers_from_excel()
stats['cadastral_numbers_count'] = len(cadastral_numbers)
stats['empty_cadastral_count'] = len(self.df) - len(cadastral_numbers)
return stats

402
main.py
View File

@@ -1,39 +1,403 @@
import tkinter as tk import tkinter as tk
from tkinter import filedialog from tkinter import filedialog, messagebox, ttk
import os import os
import threading
from pathlib import Path
# Импорт наших сервисов
from services.file_seeker import FileSeeker
from services.excel_processor import ExcelProcessor
def copy_matching_files(matching_numbers, cadastral_to_file_map, target_directory):
"""Копирует файлы с совпадающими кадастровыми номерами в целевую директорию"""
import shutil
moved_count = 0
for number in matching_numbers:
if number in cadastral_to_file_map:
source_path = Path(cadastral_to_file_map[number])
if source_path.exists():
# Создаем уникальное имя файла
dest_name = f"{number.replace(':', '_')}_{source_path.name}"
dest_path = target_directory / dest_name
try:
shutil.copy2(source_path, dest_path)
moved_count += 1
print(f"Файл скопирован в matching: {source_path.name} -> {dest_path}")
except Exception as e:
print(f"Ошибка при копировании файла {source_path}: {e}")
return moved_count
def Pars(): def Pars():
pass """Основная функция обработки"""
xml_directory = xml_entry.get().strip()
excel_file_path = excel_entry.get().strip()
# Создаем директории вывода рядом с программой
# Для PyInstaller нужно использовать sys.executable вместо __file__
import sys
if getattr(sys, 'frozen', False):
# Если программа скомпилирована PyInstaller
script_dir = Path(sys.executable).parent
else:
# Если запускается как Python скрипт
script_dir = Path(__file__).parent
output_directory = script_dir / "results"
print(f"Создаем директорию результатов: {output_directory}") # Отладочный вывод
output_directory.mkdir(exist_ok=True)
# Создаем две отдельные папки для организации файлов
all_files_dir = output_directory / "all_xml_files" # Все найденные XML файлы
matching_files_dir = output_directory / "matching_cadastral_files" # Файлы с номерами из Excel
all_files_dir.mkdir(exist_ok=True)
matching_files_dir.mkdir(exist_ok=True)
# Проверяем входные данные
if not xml_directory or not os.path.exists(xml_directory):
messagebox.showerror("Ошибка", "Выберите существующую директорию с XML файлами")
return
# Запускаем обработку в отдельном потоке
def process_files():
try:
# Показываем прогресс
progress_window = show_progress_window()
# 1. Инициализируем FileSeeker с первой папкой для всех файлов
update_progress(progress_window, "Поиск XML файлов...", 10)
seeker = FileSeeker(xml_directory, str(all_files_dir))
# 2. Ищем XML файлы
update_progress(progress_window, "Анализ найденных файлов...", 30)
xml_files = seeker.find_xml_files()
if not xml_files:
progress_window.destroy()
messagebox.showwarning("Предупреждение", "XML файлы не найдены в указанной директории")
return
# 3. Обрабатываем файлы
update_progress(progress_window, "Извлечение кадастровых номеров...", 50)
records = seeker.process_files()
if not records:
progress_window.destroy()
messagebox.showwarning("Предупреждение", "Кадастровые номера не найдены в XML файлах")
return
# 4. Создаем карту номер -> файл для быстрого поиска
cadastral_to_file_map = {}
file_to_numbers_map = {} # файл -> множество номеров для поиска смежных
for record in records:
# Для основной карты берем первое вхождение номера
if record.number not in cadastral_to_file_map:
cadastral_to_file_map[record.number] = record.file_path
# Для карты файл->номера добавляем все номера из каждого файла
if record.file_path not in file_to_numbers_map:
file_to_numbers_map[record.file_path] = set()
file_to_numbers_map[record.file_path].add(record.number)
# 5. Обработка Excel файла и организация файлов
excel_updated = False
matching_files_moved = 0
if excel_file_path and os.path.exists(excel_file_path):
update_progress(progress_window, "Обработка Excel файла...", 60)
try:
excel_processor = ExcelProcessor(excel_file_path)
if excel_processor.load_excel_file():
excel_numbers = excel_processor.get_cadastral_numbers()
# Находим пересечения между Excel и найденными номерами
found_numbers = set(cadastral_to_file_map.keys())
matching_numbers = excel_numbers.intersection(found_numbers)
if matching_numbers:
# Обновляем Excel файл информацией о найденных файлах
output_excel_path = str(output_directory / "updated_cadastral_list.xlsx")
excel_processor.update_excel_with_file_info(cadastral_to_file_map, output_excel_path)
excel_updated = True
# Копируем файлы с номерами из Excel в отдельную папку
update_progress(progress_window, "Организация файлов из Excel списка...", 70)
matching_files_moved = copy_matching_files(matching_numbers, cadastral_to_file_map, matching_files_dir)
# Копируем ВСЕ найденные файлы в общую папку
update_progress(progress_window, "Копирование всех найденных файлов...", 80)
all_files_moved = seeker.move_files_to_temp(set(cadastral_to_file_map.keys()))
else:
# Если совпадений нет, копируем все найденные файлы только в общую папку
update_progress(progress_window, "Организация всех найденных файлов...", 75)
all_files_moved = seeker.move_files_to_temp(set(cadastral_to_file_map.keys()))
else:
# Если Excel не загрузился, обрабатываем все файлы
update_progress(progress_window, "Организация всех найденных файлов...", 75)
all_files_moved = seeker.move_files_to_temp(set(cadastral_to_file_map.keys()))
except Exception as excel_error:
print(f"Ошибка при обработке Excel: {excel_error}")
update_progress(progress_window, "Организация всех найденных файлов...", 75)
all_files_moved = seeker.move_files_to_temp(set(cadastral_to_file_map.keys()))
else:
# Если Excel файл не указан, копируем все найденные файлы только в общую папку
update_progress(progress_window, "Организация всех найденных файлов...", 75)
all_files_moved = seeker.move_files_to_temp(set(cadastral_to_file_map.keys()))
# 6. Получаем статистику
update_progress(progress_window, "Подготовка результатов...", 85)
stats = seeker.get_statistics()
unique_numbers = seeker.get_unique_cadastral_numbers()
# Подготавливаем информацию о перемещенных файлах для отчета
if 'all_files_moved' in locals():
moved_files_info = all_files_moved
total_moved = len(all_files_moved)
else:
moved_files_info = {}
total_moved = 0
# 7. Создаем отчет
update_progress(progress_window, "Создание отчета...", 95)
create_report(str(output_directory), stats, unique_numbers, moved_files_info, excel_updated, excel_file_path, matching_files_moved if 'matching_files_moved' in locals() else 0)
# Закрываем окно прогресса
progress_window.destroy()
# Показываем результаты
show_results(stats, len(unique_numbers), total_moved, excel_updated, matching_files_moved if 'matching_files_moved' in locals() else 0)
except Exception as e:
if 'progress_window' in locals():
progress_window.destroy()
messagebox.showerror("Ошибка", f"Произошла ошибка при обработке: {str(e)}")
# Запускаем в отдельном потоке
thread = threading.Thread(target=process_files)
thread.daemon = True
thread.start()
def show_progress_window():
"""Создает окно с прогресс-баром"""
progress_window = tk.Toplevel(root)
progress_window.title("Обработка файлов")
progress_window.geometry("400x150")
progress_window.transient(root)
progress_window.grab_set()
# Центрируем окно
progress_window.geometry("+%d+%d" % (root.winfo_rootx() + 50, root.winfo_rooty() + 50))
# Метка с текстом
progress_label = tk.Label(progress_window, text="Инициализация...", font=font)
progress_label.pack(pady=20)
# Прогресс-бар
progress_bar = ttk.Progressbar(progress_window, length=300, mode='determinate')
progress_bar.pack(pady=10)
progress_window.label = progress_label
progress_window.bar = progress_bar
return progress_window
def update_progress(progress_window, text, value):
"""Обновляет прогресс-бар"""
if progress_window and progress_window.winfo_exists():
progress_window.label.config(text=text)
progress_window.bar['value'] = value
progress_window.update()
def create_report(output_directory, stats, unique_numbers, moved_files, excel_updated=False, excel_file_path=None, matching_files_count=0):
"""Создает текстовый отчет о результатах обработки"""
report_path = os.path.join(output_directory, "processing_report.txt")
with open(report_path, 'w', encoding='utf-8') as f:
f.write("=== ОТЧЕТ О ОБРАБОТКЕ XML ФАЙЛОВ ===\n\n")
f.write(f"Общая статистика:\n")
f.write(f"- Всего XML файлов найдено: {stats['total_xml_files']}\n")
f.write(f"- Файлов с кадастровыми номерами: {stats['files_with_numbers']}\n")
f.write(f"- Всего кадастровых номеров: {stats['total_cadastral_numbers']}\n")
f.write(f"- Уникальных номеров: {stats['unique_cadastral_numbers']}\n")
f.write(f"- Файлов скопировано в 'all_xml_files': {len(moved_files)}\n")
if matching_files_count > 0:
f.write(f"- Файлов из Excel списка скопировано в 'matching_cadastral_files': {matching_files_count}\n")
f.write("\n📁 Структура результатов:\n")
f.write("- all_xml_files/ - все найденные XML файлы с кадастровыми номерами\n")
f.write("- matching_cadastral_files/ - файлы с номерами из Excel списка\n")
f.write("- processing_report.txt - данный отчет\n")
if excel_updated and excel_file_path:
f.write(f"- updated_cadastral_list.xlsx - обновленный Excel файл\n\n")
f.write(f"Excel обработка:\n")
f.write(f"- Исходный Excel файл: {Path(excel_file_path).name}\n")
f.write(f"- Обновленный файл создан: updated_cadastral_list.xlsx\n")
f.write(f"- Добавлены колонки:\n")
f.write(f"'Найденные файлы' - файлы с кадастровыми номерами\n")
f.write(f"'Смежные кадастровые номера' - другие номера из тех же файлов\n")
f.write(f"'Кадастровый блок' - первые 3 части номера\n\n")
else:
f.write("\n")
f.write("Найденные кадастровые номера:\n")
for number in sorted(unique_numbers):
f.write(f"- {number}\n")
if moved_files:
f.write(f"\nСкопированные файлы:\n")
for number, file_path in moved_files.items():
f.write(f"- {number}: {Path(file_path).name}\n")
def show_results(stats, unique_count, total_moved, excel_updated=False, matching_files_count=0):
"""Показывает окно с результатами обработки"""
excel_info = ""
if excel_updated:
excel_info = ("\n📊 Excel файл обновлен с дополнительной информацией:\n"
"• Файлы с найденными номерами\n"
"• Смежные кадастровые номера из тех же файлов\n"
"• Кадастровые блоки")
matching_info = ""
if matching_files_count > 0:
matching_info = f"\n📋 Файлов из Excel списка: {matching_files_count}"
result_text = (
f"✅ Обработка завершена успешно!\n\n"
f"📊 Результаты:\n"
f"• XML файлов обработано: {stats['total_xml_files']}\n"
f"• Файлов с номерами: {stats['files_with_numbers']}\n"
f"• Уникальных кадастровых номеров: {unique_count}\n"
f"Всего файлов скопировано: {total_moved}"
f"{matching_info}\n\n"
f"📁 Результаты сохранены в папке 'results':\n"
f"• all_xml_files/ - все найденные файлы\n"
f"• matching_cadastral_files/ - файлы из Excel\n"
f"📋 Создан подробный отчет: processing_report.txt"
f"{excel_info}"
)
messagebox.showinfo("Результаты обработки", result_text)
root = tk.Tk() root = tk.Tk()
root.title("Parser12") root.title("Parser14")
font = ("Arial", 12) font = ("Arial", 12)
root.configure(bg="#B85C38") root.configure(bg="#B85C38")
options = [""] root.geometry("600x500")
# Устанавливаем минимальный размер окна
root.minsize(500, 250)
#путь для сохранения файлов #путь для сохранения файлов
def open_directory(event, entry): def open_directory(event, entry):
directory_path = filedialog.askdirectory() directory_path = filedialog.askdirectory()
entry.delete(0, tk.END) if directory_path: # Проверяем что путь выбран
entry.insert(0, directory_path) entry.delete(0, tk.END)
entry.insert(0, directory_path)
# Функция для выбора Excel файла
def open_excel_file(event, entry):
file_path = filedialog.askopenfilename(
title="Выберите Excel файл",
filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
)
if file_path:
entry.delete(0, tk.END)
entry.insert(0, file_path)
# Создаем главный фрейм
main_frame = tk.Frame(root, bg="#B85C38")
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
# Заголовок
title_label = tk.Label(main_frame,
text="🏗️ Парсер кадастровых номеров из XML файлов",
font=("Arial", 14, "bold"),
bg="#B85C38",
fg="white")
title_label.pack(pady=(0, 20))
# первое поле с списком # первое поле с списком
xml_label = tk.Label(root, text="директория xml", font=font) xml_label = tk.Label(main_frame, text="📁 Директория с XML файлами:", font=font, bg="#B85C38", fg="white")
xml_label.pack(anchor="w") xml_label.pack(anchor="w", pady=(0, 5))
xml_entry = tk.Entry(root, font=font, width=70) xml_entry = tk.Entry(main_frame, font=font, width=70)
xml_entry.pack(anchor="w") xml_entry.pack(anchor="w", fill=tk.X, pady=(0, 15))
xml_entry.bind("<Button-1>", lambda event: open_directory(event, xml_entry)) xml_entry.bind("<Button-1>", lambda event: open_directory(event, xml_entry))
directory_label = tk.Label(root, text="Выберите директорию для сохранения:", font=font) # Поле для Excel файла (опционально)
directory_label.pack(anchor="w") excel_label = tk.Label(main_frame, text="📊 Excel файл со списком номеров (опционально):", font=font, bg="#B85C38", fg="white")
directory_entry = tk.Entry(root, font=font, width=70) excel_label.pack(anchor="w", pady=(0, 5))
directory_entry.pack(anchor="w") excel_entry = tk.Entry(main_frame, font=font, width=70)
directory_entry.bind("<Button-1>", lambda event: open_directory(event, directory_entry)) excel_entry.pack(anchor="w", fill=tk.X, pady=(0, 15))
excel_entry.bind("<Button-1>", lambda event: open_excel_file(event, excel_entry))
# кнопка # Информация о месте сохранения
empty_button = tk.Button(root, text="->", command=Pars, font=font) info_label = tk.Label(main_frame,
empty_button.pack(side="right", padx=10, pady=10) text="💾 Результаты будут сохранены в папке 'results' рядом с программой",
font=("Arial", 10),
bg="#B85C38",
fg="#FFE4B5")
info_label.pack(anchor="w", pady=(0, 20))
# Фрейм для кнопок
button_frame = tk.Frame(main_frame, bg="#B85C38")
button_frame.pack(fill=tk.X)
# кнопка запуска
start_button = tk.Button(button_frame,
text="🚀 Начать обработку",
command=Pars,
font=("Arial", 12, "bold"),
bg="#2E8B57",
fg="white",
padx=20,
pady=10)
start_button.pack(side="right", padx=(10, 0))
# Кнопка помощи
help_button = tk.Button(button_frame,
text=" Справка",
command=lambda: show_help(),
font=font,
bg="#4682B4",
fg="white",
padx=15,
pady=10)
help_button.pack(side="right")
def show_help():
"""Показывает справочную информацию"""
help_text = """
🔍 Инструкция по использованию:
1. Выберите директорию с XML файлами
2. (Опционально) Выберите Excel файл со списком кадастровых номеров
3. Нажмите "Начать обработку"
📋 Программа выполнит:
• Поиск всех XML файлов в указанной директории
• Извлечение кадастровых номеров из файлов
• Организацию файлов по номерам
• Обновление Excel файла (если указан) с дополнительными колонками:
- Найденные файлы (где обнаружен номер)
- Смежные кадастровые номера (из тех же файлов)
- Кадастровый блок (первые 3 части номера)
• Создание подробного отчета
📁 Результаты сохраняются в папке 'results' рядом с программой:
• all_xml_files/ - ВСЕ найденные XML файлы с кадастровыми номерами
• matching_cadastral_files/ - файлы с номерами из Excel списка
• updated_cadastral_list.xlsx - обновленный Excel (если указан)
• processing_report.txt - подробный отчет
⚡ Поддерживаемые форматы номеров: XX:XX:XXXXXXX:XXX
"""
messagebox.showinfo("Справка", help_text)
root.mainloop() root.mainloop()

1
models/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Модели данных

View File

@@ -0,0 +1,14 @@
"""
Модель для хранения информации о кадастровом номере.
"""
class CadastralRecord:
def __init__(self, number: str, file_path: str):
self.number = number
self.file_path = file_path
def __str__(self):
return f"Кадастровый номер: {self.number}, файл: {self.file_path}"
def __repr__(self):
return f"CadastralRecord('{self.number}', '{self.file_path}')"

71
models/xml_file_info.py Normal file
View File

@@ -0,0 +1,71 @@
"""
Модель для хранения информации об XML файле.
"""
import re
from pathlib import Path
from typing import List, Set, Optional
from lxml import etree
class XMLFileInfo:
def __init__(self, file_path: str):
self.file_path = Path(file_path)
self.cadastral_numbers: Set[str] = set()
def extract_cadastral_numbers(self) -> Set[str]:
"""Извлекает кадастровые номера из XML файла"""
if not self.file_path.exists():
return set()
try:
# Парсим XML файл
tree = etree.parse(str(self.file_path))
root = tree.getroot()
# Используем улучшенный алгоритм извлечения
return self._extract_cadastral_numbers_enhanced(root)
except Exception as e:
print(f"Ошибка при обработке файла {self.file_path}: {e}")
return set()
def _extract_cadastral_numbers_enhanced(self, root) -> Set[str]:
"""Улучшенный алгоритм извлечения кадастровых номеров"""
cadastral_numbers = set()
# 1. Поиск по известным тегам
tags_to_search = ['cad_number', 'land_cad_number', 'cadastral_number', 'parcel_cad_number']
for tag in tags_to_search:
elements = root.xpath(f".//{tag}")
for elem in elements:
if elem.text and self._is_valid_cadastral_number(elem.text):
cadastral_numbers.add(elem.text.strip())
# 2. Поиск в атрибутах
for elem in root.iter():
for attr_name, attr_value in elem.attrib.items():
if any(keyword in attr_name.lower() for keyword in ['cad', 'cadastral', 'кадастр']):
if self._is_valid_cadastral_number(attr_value):
cadastral_numbers.add(attr_value.strip())
# 3. Regex поиск по тексту
text_content = etree.tostring(root, encoding='unicode', method='text')
if text_content:
regex_matches = re.findall(r'\d{1,2}:\d{1,2}:\d{6,8}:\d{1,5}', text_content)
for match in regex_matches:
if self._is_valid_cadastral_number(match):
cadastral_numbers.add(match)
self.cadastral_numbers = cadastral_numbers
return cadastral_numbers
def _is_valid_cadastral_number(self, text: str) -> bool:
"""Проверяет, является ли строка валидным кадастровым номером"""
if not text:
return False
# Улучшенный паттерн для российских кадастровых номеров
pattern = r'^\d{1,2}:\d{1,2}:\d{6,8}:\d{1,5}$'
return bool(re.match(pattern, text.strip()))
def __str__(self):
return f"XML файл: {self.file_path.name}, номеров: {len(self.cadastral_numbers)}"

View File

@@ -1,9 +1,20 @@
[project] [project]
name = "kadastr14" name = "kadastr14"
version = "0.1.0" version = "2.0.0"
description = "Add your description here" description = "Парсер кадастровых номеров из XML файлов ЕГРН"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" authors = [
{name = "Developer", email = "dev@example.com"}
]
dependencies = [ dependencies = [
"lxml>=6.0.0", "lxml>=6.0.0",
"openpyxl>=3.1.5",
"pandas>=2.3.1",
"pathlib2>=2.3.0",
]
requires-python = ">=3.12"
[dependency-groups]
dev = [
"pyinstaller>=6.15.0",
] ]

View File

@@ -1,505 +0,0 @@
import os
import shutil
import tempfile
import zipfile
from pathlib import Path
from lxml import etree
import re
class CadastralRecord:
"""Класс для хранения информации о кадастровом номере и связанных с ним данных."""
def __init__(self, cadastral_number, source_xml_info, temp_file_path=None):
self.cadastral_number = cadastral_number # Основной кадастровый номер
self.source_xml_info = source_xml_info # Ссылка на XMLFileInfo где найден
self.temp_file_path = temp_file_path # Путь к файлу во временной папке
self.quarter_number = self._extract_quarter_number() # Номер квартала
self.related_numbers = [] # Смежные кадастровые номера из того же файла
def _extract_quarter_number(self):
"""Извлекает номер квартала из кадастрового номера."""
# Квартал - это первые три части номера (NN:NN:NNNNNNN)
parts = self.cadastral_number.split(':')
if len(parts) >= 3:
return ':'.join(parts[:3])
return None
def set_related_numbers(self, all_numbers_from_file):
"""Устанавливает список смежных кадастровых номеров из того же файла."""
# Убираем сам номер из списка смежных
self.related_numbers = [num for num in all_numbers_from_file if num != self.cadastral_number]
def get_object_type(self):
"""Определяет тип объекта по кадастровому номеру."""
parts = self.cadastral_number.split(':')
if len(parts) == 4:
# Полный кадастровый номер объекта недвижимости
return "Объект недвижимости"
elif len(parts) == 3:
# Номер квартала
return "Квартал"
else:
return "Неизвестный тип"
def __str__(self):
return f"CadastralRecord({self.cadastral_number}, квартал: {self.quarter_number})"
def __repr__(self):
return self.__str__()
class XMLFileInfo:
"""Класс для хранения информации о XML файле и его происхождении."""
def __init__(self, original_name, content, source_path, archive_name=None):
self.original_name = original_name # Оригинальное имя файла
self.content = content # Содержимое файла (bytes)
self.source_path = source_path # Полный путь к источнику (файлу или архиву)
self.archive_name = archive_name # Имя архива, если файл из архива
self.is_from_archive = archive_name is not None
self.cadastral_numbers = [] # Список найденных кадастровых номеров
self.analysis_completed = False # Флаг выполненного анализа
def analyze_xml_content(self):
"""Анализирует содержимое XML файла и извлекает кадастровые номера."""
if self.analysis_completed:
return self.cadastral_numbers
try:
# Парсим XML из bytes
root = etree.fromstring(self.content)
self.cadastral_numbers = self._extract_cadastral_numbers(root)
self.analysis_completed = True
except Exception as e:
print(f"Ошибка при анализе XML файла {self.original_name}: {e}")
self.cadastral_numbers = []
self.analysis_completed = True
return self.cadastral_numbers
def _extract_cadastral_numbers(self, element):
"""Извлекает кадастровые номера из XML элемента."""
cadastral_numbers = set()
# Ищем кадастровые номера в атрибутах
if element.attrib:
for attr_name, attr_value in element.attrib.items():
if 'CadastralNumber' in attr_name or 'cadastralnumber' in attr_name.lower():
if self._is_valid_cadastral_number(attr_value):
cadastral_numbers.add(attr_value)
elif 'Definition' in attr_name:
if self._is_valid_cadastral_number(attr_value):
cadastral_numbers.add(attr_value)
# Ищем кадастровые номера в тексте элементов
if element.text and element.text.strip():
text = element.text.strip()
if self._is_valid_cadastral_number(text):
cadastral_numbers.add(text)
# Рекурсивно проходим по всем дочерним элементам
for child in element:
# Проверяем название тега на наличие ключевых слов
tag_lower = child.tag.lower()
if any(keyword in tag_lower for keyword in ['cad_number', 'cadastralnumber', 'quarter_cad_number', 'land_cad_number']):
if child.text and child.text.strip():
text = child.text.strip()
if self._is_valid_cadastral_number(text):
cadastral_numbers.add(text)
# Рекурсивно анализируем дочерние элементы
child_numbers = self._extract_cadastral_numbers(child)
cadastral_numbers.update(child_numbers)
return list(cadastral_numbers)
def _is_valid_cadastral_number(self, value):
"""Проверяет, является ли строка валидным кадастровым номером."""
if not value or not isinstance(value, str):
return False
# Паттерн для кадастрового номера: NN:NN:NNNNNNN:NNNN
# где N - цифра, количество цифр может варьироваться
pattern = r'^\d{1,2}:\d{1,2}:\d{6,7}:\d{1,4}$'
return bool(re.match(pattern, value.strip()))
def get_cadastral_numbers(self):
"""Возвращает список кадастровых номеров (с анализом если не выполнен)."""
if not self.analysis_completed:
self.analyze_xml_content()
return self.cadastral_numbers
def create_cadastral_records(self, temp_file_path=None):
"""Создает объекты CadastralRecord для каждого найденного кадастрового номера."""
cadastral_numbers = self.get_cadastral_numbers()
records = []
for number in cadastral_numbers:
record = CadastralRecord(number, self, temp_file_path)
record.set_related_numbers(cadastral_numbers)
records.append(record)
return records
def get_display_name(self):
"""Возвращает удобное для отображения имя файла."""
if self.is_from_archive:
return f"{self.original_name} (из архива {self.archive_name})"
else:
return self.original_name
def get_safe_filename(self):
"""Возвращает безопасное имя файла для сохранения."""
if self.is_from_archive:
archive_stem = Path(self.archive_name).stem
return f"ARCHIVE_{archive_stem}_{Path(self.original_name).stem}.xml"
else:
return self.original_name
def save_to_temp(self, temp_dir):
"""Сохраняет файл во временную папку."""
safe_name = self.get_safe_filename()
dest_path = temp_dir / safe_name
# Если файл уже существует, добавляем номер
counter = 1
while dest_path.exists():
name_parts = dest_path.stem, counter, dest_path.suffix
dest_path = temp_dir / f"{name_parts[0]}_{name_parts[1]}{name_parts[2]}"
counter += 1
dest_path.write_bytes(self.content)
return dest_path
class FileSeeker:
def __init__(self, search_dir):
self.search_dir = Path(search_dir)
# Создаем временную папку рядом с программой
program_dir = Path(__file__).parent
self.temp_dir = program_dir / "kadastr14_temp"
self.temp_dir.mkdir(exist_ok=True)
print(f"Временная папка создана: {self.temp_dir}")
def find_xml_files(self):
"""Ищет все XML-файлы рекурсивно в директории поиска и возвращает объекты XMLFileInfo."""
xml_files = list(self.search_dir.rglob('*.xml'))
xml_file_objects = []
print(f"Найдено XML файлов: {len(xml_files)}")
for file_path in xml_files:
print(f" - {file_path}")
try:
content = file_path.read_bytes()
xml_info = XMLFileInfo(
original_name=file_path.name,
content=content,
source_path=str(file_path)
)
xml_file_objects.append(xml_info)
except Exception as e:
print(f" Ошибка при чтении {file_path}: {e}")
return xml_file_objects
def find_archives(self):
"""Ищет все архивы (ZIP) рекурсивно в директории поиска."""
zip_files = list(self.search_dir.rglob('*.zip'))
print(f"Найдено ZIP архивов: {len(zip_files)}")
for file in zip_files:
print(f" - {file}")
return zip_files
def extract_xml_from_archive(self, archive_path):
"""Извлекает XML файлы из архива и возвращает объекты XMLFileInfo."""
xml_file_objects = []
archive_name = archive_path.name
try:
with zipfile.ZipFile(archive_path, 'r') as zip_ref:
# Получаем список файлов в архиве
file_list = zip_ref.namelist()
xml_files_in_archive = [f for f in file_list if f.lower().endswith('.xml')]
print(f" В архиве {archive_name} найдено XML файлов: {len(xml_files_in_archive)}")
for xml_file in xml_files_in_archive:
try:
# Читаем содержимое файла из архива
content = zip_ref.read(xml_file)
xml_info = XMLFileInfo(
original_name=Path(xml_file).name,
content=content,
source_path=str(archive_path),
archive_name=archive_name
)
xml_file_objects.append(xml_info)
print(f" Найден в архиве: {xml_file}")
except Exception as e:
print(f" Ошибка при извлечении {xml_file}: {e}")
except Exception as e:
print(f" Ошибка при обработке архива {archive_path}: {e}")
return xml_file_objects
def copy_to_temp(self):
"""Собирает все XML файлы (обычные и из архивов) и сохраняет их во временную папку."""
all_xml_objects = []
# Сначала собираем обычные XML файлы
print(f"\nПоиск обычных XML файлов...")
xml_objects = self.find_xml_files()
all_xml_objects.extend(xml_objects)
# Теперь обрабатываем архивы
print(f"\nПоиск и обработка архивов...")
archives = self.find_archives()
for archive_path in archives:
print(f"\nОбработка архива: {archive_path.name}")
archive_xml_objects = self.extract_xml_from_archive(archive_path)
all_xml_objects.extend(archive_xml_objects)
# Сохраняем все найденные XML файлы во временную папку
print(f"\nСохранение XML файлов в: {self.temp_dir}")
saved_files = []
for xml_obj in all_xml_objects:
try:
saved_path = xml_obj.save_to_temp(self.temp_dir)
saved_files.append(saved_path)
print(f" Сохранен: {xml_obj.get_display_name()} -> {saved_path.name}")
except Exception as e:
print(f" Ошибка при сохранении {xml_obj.get_display_name()}: {e}")
print(f"\nВсего найдено XML файлов: {len(all_xml_objects)}")
print(f"Успешно сохранено: {len(saved_files)}")
return all_xml_objects, saved_files
def collect_all_cadastral_records(self, xml_objects, saved_files):
"""Собирает все кадастровые записи из всех XML файлов."""
print(f"\n{'='*80}")
print(f"СБОР КАДАСТРОВЫХ ЗАПИСЕЙ")
print(f"{'='*80}")
all_records = []
file_path_mapping = {}
# Создаем маппинг XML объектов к путям сохраненных файлов
for i, xml_obj in enumerate(xml_objects):
if i < len(saved_files):
file_path_mapping[xml_obj] = saved_files[i]
# Собираем записи из каждого файла
for xml_obj in xml_objects:
temp_file_path = file_path_mapping.get(xml_obj)
records = xml_obj.create_cadastral_records(temp_file_path)
all_records.extend(records)
if records:
print(f"📄 {xml_obj.get_display_name()}: {len(records)} кадастровых записей")
print(f"\nВсего собрано кадастровых записей: {len(all_records)}")
return all_records
def print_cadastral_records_summary(self, records):
"""Выводит сводку по всем кадастровым записям."""
print(f"\n{'='*80}")
print(f"СВОДКА ПО КАДАСТРОВЫМ ЗАПИСЯМ")
print(f"{'='*80}")
if not records:
print("Кадастровые записи не найдены.")
return
# Группируем по типам объектов
by_type = {}
by_quarter = {}
unique_numbers = set()
for record in records:
obj_type = record.get_object_type()
if obj_type not in by_type:
by_type[obj_type] = []
by_type[obj_type].append(record)
if record.quarter_number:
if record.quarter_number not in by_quarter:
by_quarter[record.quarter_number] = []
by_quarter[record.quarter_number].append(record)
unique_numbers.add(record.cadastral_number)
print(f"📊 Общая статистика:")
print(f" Всего записей: {len(records)}")
print(f" Уникальных номеров: {len(unique_numbers)}")
print(f" Кварталов: {len(by_quarter)}")
print(f"\n📋 По типам объектов:")
for obj_type, type_records in by_type.items():
print(f" {obj_type}: {len(type_records)} записей")
print(f"\n🏘️ По кварталам (топ-10):")
sorted_quarters = sorted(by_quarter.items(), key=lambda x: len(x[1]), reverse=True)
for quarter, quarter_records in sorted_quarters[:10]:
print(f" {quarter}: {len(quarter_records)} объектов")
def print_detailed_cadastral_records(self, records, max_records=20):
"""Выводит детальную информацию о кадастровых записях."""
print(f"\n{'='*80}")
print(f"ДЕТАЛЬНАЯ ИНФОРМАЦИЯ О КАДАСТРОВЫХ ЗАПИСЯХ (первые {max_records})")
print(f"{'='*80}")
for i, record in enumerate(records[:max_records], 1):
print(f"\n{i}. 🏠 {record.cadastral_number}")
print(f" 📍 Тип: {record.get_object_type()}")
if record.quarter_number:
print(f" 🏘️ Квартал: {record.quarter_number}")
print(f" 📁 Источник: {record.source_xml_info.get_display_name()}")
print(f" 📂 Оригинальный файл: {record.source_xml_info.source_path}")
if record.temp_file_path:
print(f" 💾 Временный файл: {record.temp_file_path}")
if record.related_numbers:
print(f" 🔗 Смежные номера ({len(record.related_numbers)}):")
for j, related in enumerate(record.related_numbers[:3], 1):
print(f" {j}. {related}")
if len(record.related_numbers) > 3:
print(f" ... и еще {len(record.related_numbers) - 3} номеров")
else:
print(f" 🔗 Смежные номера: нет")
if len(records) > max_records:
print(f"\n... и еще {len(records) - max_records} записей")
def print_all_objects_info(self, xml_objects):
"""Выводит подробную информацию о всех найденных XML объектах."""
print(f"\n{'='*80}")
print(f"ПОДРОБНАЯ ИНФОРМАЦИЯ О ВСЕХ НАЙДЕННЫХ XML ОБЪЕКТАХ")
print(f"{'='*80}")
print(f"Всего объектов: {len(xml_objects)}")
if not xml_objects:
print("Объекты не найдены.")
return
# Статистика по источникам
from_files = sum(1 for obj in xml_objects if not obj.is_from_archive)
from_archives = sum(1 for obj in xml_objects if obj.is_from_archive)
print(f"\nСтатистика по источникам:")
print(f" - Из обычных файлов: {from_files}")
print(f" - Из архивов: {from_archives}")
# Группируем по архивам для удобства
archive_groups = {}
regular_files = []
for obj in xml_objects:
if obj.is_from_archive:
if obj.archive_name not in archive_groups:
archive_groups[obj.archive_name] = []
archive_groups[obj.archive_name].append(obj)
else:
regular_files.append(obj)
# Выводим обычные файлы
if regular_files:
print(f"\n{'-'*60}")
print(f"ОБЫЧНЫЕ XML ФАЙЛЫ ({len(regular_files)} файлов)")
print(f"{'-'*60}")
for i, obj in enumerate(regular_files, 1):
self._print_object_details(obj, i)
# Выводим файлы из архивов
if archive_groups:
print(f"\n{'-'*60}")
print(f"XML ФАЙЛЫ ИЗ АРХИВОВ ({len(archive_groups)} архивов, {from_archives} файлов)")
print(f"{'-'*60}")
counter = 1
for archive_name, objects in archive_groups.items():
print(f"\n📦 АРХИВ: {archive_name}")
print(f" Путь: {objects[0].source_path}")
print(f" XML файлов в архиве: {len(objects)}")
for obj in objects:
self._print_object_details(obj, counter, indent=" ")
counter += 1
print(f"\n{'='*80}")
print(f"ИТОГО: {len(xml_objects)} XML файлов обработано")
print(f"{'='*80}")
def _print_object_details(self, obj, number, indent=""):
"""Выводит детальную информацию об одном XML объекте."""
print(f"\n{indent}{number}. {obj.get_display_name()}")
print(f"{indent} 📄 Исходное имя: {obj.original_name}")
print(f"{indent} 📁 Источник: {obj.source_path}")
print(f"{indent} 💾 Размер содержимого: {len(obj.content):,} байт")
print(f"{indent} 🗂️ Безопасное имя: {obj.get_safe_filename()}")
# Запускаем анализ кадастровых номеров если не выполнен
cadastral_numbers = obj.get_cadastral_numbers() # Это запустит анализ если нужно
cadastral_count = len(cadastral_numbers)
print(f"{indent} 🔍 Анализ завершен: найдено {cadastral_count} кадастровых номеров")
if cadastral_count > 0:
# Показываем первые 3 номера
for i, num in enumerate(cadastral_numbers[:3], 1):
print(f"{indent} {i}. {num}")
if cadastral_count > 3:
print(f"{indent} ... и еще {cadastral_count - 3} номеров")
else:
print(f"{indent} ❌ Кадастровые номера не найдены")
if __name__ == "__main__":
# Пример использования - ищем во всей папке с примерами
search_path = r"C:\Users\USER\Downloads\14. выборка выписок из ЕГРН по списку КН"
print(f"Начинаем поиск XML файлов в: {search_path}")
print("Поиск будет выполняться рекурсивно во всех подпапках...")
seeker = FileSeeker(search_path)
xml_objects, saved_files = seeker.copy_to_temp()
print(f"\nГотово! Временная папка: {seeker.temp_dir}")
print(f"Найдено XML объектов: {len(xml_objects)}")
print(f"Сохранено файлов: {len(saved_files)}")
# Собираем все кадастровые записи
cadastral_records = seeker.collect_all_cadastral_records(xml_objects, saved_files)
# Выводим сводку
seeker.print_cadastral_records_summary(cadastral_records)
# Выводим детальную информацию
seeker.print_detailed_cadastral_records(cadastral_records, max_records=10)
# Дополнительный анализ - находим записи с наибольшим количеством смежных номеров
if cadastral_records:
print(f"\n{'='*80}")
print(f"ЗАПИСИ С НАИБОЛЬШИМ КОЛИЧЕСТВОМ СМЕЖНЫХ НОМЕРОВ")
print(f"{'='*80}")
# Сортируем по количеству смежных номеров
sorted_by_related = sorted(cadastral_records, key=lambda x: len(x.related_numbers), reverse=True)
for i, record in enumerate(sorted_by_related[:5], 1):
print(f"\n{i}. {record.cadastral_number}")
print(f" Смежных номеров: {len(record.related_numbers)}")
print(f" Источник: {record.source_xml_info.get_display_name()}")
if record.related_numbers:
print(f" Связанные номера: {', '.join(record.related_numbers[:5])}")
if len(record.related_numbers) > 5:
print(f" ... и еще {len(record.related_numbers) - 5} номеров")
print(f"\n{'='*80}")
print(f"АНАЛИЗ ЗАВЕРШЕН")
print(f"{'='*80}")
print(f"📊 Всего кадастровых записей: {len(cadastral_records)}")
print(f"📁 Временная папка: {seeker.temp_dir}")
print(f"💾 Сохранено XML файлов: {len(saved_files)}")

1
services/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Сервисы

171
services/excel_processor.py Normal file
View File

@@ -0,0 +1,171 @@
"""
Сервис для работы с Excel файлами.
"""
import pandas as pd
from pathlib import Path
from typing import List, Set, Dict, Optional
class ExcelProcessor:
def __init__(self, excel_file_path: str):
self.excel_file_path = Path(excel_file_path)
self.cadastral_numbers: Set[str] = set()
self.df: Optional[pd.DataFrame] = None
self.cadastral_column: Optional[str] = None
def load_excel_file(self) -> bool:
"""Загружает Excel файл и находит колонку с кадастровыми номерами"""
try:
if not self.excel_file_path.exists():
print(f"Excel файл {self.excel_file_path} не найден")
return False
# Пробуем разные форматы Excel файлов
try:
self.df = pd.read_excel(self.excel_file_path, engine='openpyxl')
except:
self.df = pd.read_excel(self.excel_file_path, engine='xlrd')
# Ищем колонку с кадастровыми номерами
self.cadastral_column = self._find_cadastral_column()
if self.cadastral_column:
# Извлекаем кадастровые номера
self._extract_cadastral_numbers()
print(f"Загружено {len(self.cadastral_numbers)} кадастровых номеров из Excel")
return True
else:
print("Не найдена колонка с кадастровыми номерами в Excel файле")
return False
except Exception as e:
print(f"Ошибка при загрузке Excel файла: {e}")
return False
def _find_cadastral_column(self) -> Optional[str]:
"""Находит колонку с кадастровыми номерами"""
if self.df is None:
return None
# Ключевые слова для поиска колонки
keywords = ['кадастр', 'номер', 'cadastral', 'number', 'кн', 'cad']
for column in self.df.columns:
column_str = str(column).lower()
if any(keyword in column_str for keyword in keywords):
return column
# Если не найдено по названию, ищем по содержимому
for column in self.df.columns:
# Проверяем первые несколько значений в колонке
sample_values = self.df[column].dropna().head(5)
cadastral_count = 0
for value in sample_values:
if self._is_cadastral_number_format(str(value)):
cadastral_count += 1
# Если больше половины значений похожи на кадастровые номера
if cadastral_count >= len(sample_values) * 0.5:
return column
return None
def _extract_cadastral_numbers(self):
"""Извлекает кадастровые номера из найденной колонки"""
if self.df is None or self.cadastral_column is None:
return
cadastral_numbers = set()
for value in self.df[self.cadastral_column].dropna():
value_str = str(value).strip()
if self._is_cadastral_number_format(value_str):
cadastral_numbers.add(value_str)
self.cadastral_numbers = cadastral_numbers
def _is_cadastral_number_format(self, text: str) -> bool:
"""Проверяет, соответствует ли текст формату кадастрового номера"""
import re
pattern = r'^\d{1,2}:\d{1,2}:\d{6,8}:\d{1,5}$'
return bool(re.match(pattern, text))
def update_excel_with_file_info(self, cadastral_to_file_map: Dict[str, str],
output_file_path: str = None) -> bool:
"""Обновляет Excel файл информацией о найденных файлах и смежных номерах"""
try:
if self.df is None or self.cadastral_column is None:
print("Excel файл не загружен или не найдена колонка с номерами")
return False
# Создаем карту файл -> множество кадастровых номеров для поиска смежных
file_to_numbers_map = {}
for number, file_path in cadastral_to_file_map.items():
if file_path not in file_to_numbers_map:
file_to_numbers_map[file_path] = set()
file_to_numbers_map[file_path].add(number)
# Добавляем новые колонки
self.df['Найденные файлы'] = ''
self.df['Смежные кадастровые номера'] = ''
self.df['Кадастровый блок'] = ''
# Заполняем информацию для каждой строки
for index, row in self.df.iterrows():
cadastral_number = str(row[self.cadastral_column]).strip()
if cadastral_number in cadastral_to_file_map:
# 1. Получаем файл(ы) где найден номер
main_file_path = cadastral_to_file_map[cadastral_number]
file_name = Path(main_file_path).name
self.df.at[index, 'Найденные файлы'] = file_name
# 2. Находим все смежные кадастровые номера из того же файла
if main_file_path in file_to_numbers_map:
all_numbers_in_file = file_to_numbers_map[main_file_path]
# Исключаем текущий номер из списка смежных
adjacent_numbers = all_numbers_in_file - {cadastral_number}
if adjacent_numbers:
self.df.at[index, 'Смежные кадастровые номера'] = ', '.join(sorted(adjacent_numbers))
# 3. Определяем кадастровый блок (первые три части номера)
cadastral_block = self._extract_cadastral_block(cadastral_number)
if cadastral_block:
self.df.at[index, 'Кадастровый блок'] = cadastral_block
# Сохраняем обновленный файл
output_path = output_file_path or str(self.excel_file_path.with_suffix('.updated.xlsx'))
self.df.to_excel(output_path, index=False, engine='openpyxl')
print(f"Обновленный Excel файл сохранен: {output_path}")
return True
except Exception as e:
print(f"Ошибка при обновлении Excel файла: {e}")
return False
def _extract_cadastral_block(self, cadastral_number: str) -> str:
"""Извлекает кадастровый блок из номера (первые три части)"""
try:
parts = cadastral_number.split(':')
if len(parts) >= 3:
return ':'.join(parts[:3])
return cadastral_number
except:
return cadastral_number
def get_cadastral_numbers(self) -> Set[str]:
"""Возвращает множество кадастровых номеров из Excel"""
return self.cadastral_numbers.copy()
def get_statistics(self) -> Dict[str, any]:
"""Возвращает статистику по Excel файлу"""
if self.df is None:
return {}
return {
'total_rows': len(self.df),
'cadastral_column': self.cadastral_column,
'cadastral_numbers_count': len(self.cadastral_numbers),
'columns': list(self.df.columns)
}

91
services/file_seeker.py Normal file
View File

@@ -0,0 +1,91 @@
"""
Сервис для поиска и обработки XML файлов.
"""
import os
import shutil
import tempfile
from pathlib import Path
from typing import List, Set, Dict
from models.xml_file_info import XMLFileInfo
from models.cadastral_record import CadastralRecord
class FileSeeker:
def __init__(self, base_directory: str, temp_directory: str = None):
self.base_directory = Path(base_directory)
self.temp_directory = Path(temp_directory) if temp_directory else Path(tempfile.mkdtemp(prefix="kadastr14_"))
self.xml_files: List[XMLFileInfo] = []
self.cadastral_records: List[CadastralRecord] = []
# Создаем временную директорию если её нет
self.temp_directory.mkdir(parents=True, exist_ok=True)
def find_xml_files(self) -> List[XMLFileInfo]:
"""Находит все XML файлы в базовой директории"""
xml_files = []
if not self.base_directory.exists():
print(f"Директория {self.base_directory} не существует")
return xml_files
for file_path in self.base_directory.rglob("*.xml"):
if file_path.is_file():
xml_info = XMLFileInfo(str(file_path))
xml_files.append(xml_info)
self.xml_files = xml_files
print(f"Найдено XML файлов: {len(xml_files)}")
return xml_files
def process_files(self) -> List[CadastralRecord]:
"""Обрабатывает все найденные XML файлы и извлекает кадастровые номера"""
all_records = []
for xml_file in self.xml_files:
cadastral_numbers = xml_file.extract_cadastral_numbers()
for number in cadastral_numbers:
record = CadastralRecord(number, str(xml_file.file_path))
all_records.append(record)
self.cadastral_records = all_records
print(f"Всего найдено кадастровых номеров: {len(all_records)}")
return all_records
def get_unique_cadastral_numbers(self) -> Set[str]:
"""Возвращает уникальные кадастровые номера"""
return {record.number for record in self.cadastral_records}
def move_files_to_temp(self, cadastral_numbers_to_move: Set[str]) -> Dict[str, str]:
"""Перемещает файлы с указанными кадастровыми номерами во временную папку"""
moved_files = {}
for record in self.cadastral_records:
if record.number in cadastral_numbers_to_move:
source_path = Path(record.file_path)
if source_path.exists():
# Создаем уникальное имя для файла во временной папке
dest_name = f"{record.number.replace(':', '_')}_{source_path.name}"
dest_path = self.temp_directory / dest_name
try:
shutil.copy2(source_path, dest_path)
moved_files[record.number] = str(dest_path)
print(f"Файл скопирован: {source_path.name} -> {dest_path}")
except Exception as e:
print(f"Ошибка при копировании файла {source_path}: {e}")
return moved_files
def get_statistics(self) -> Dict[str, int]:
"""Возвращает статистику обработки"""
return {
'total_xml_files': len(self.xml_files),
'files_with_numbers': len([f for f in self.xml_files if len(f.cadastral_numbers) > 0]),
'total_cadastral_numbers': len(self.cadastral_records),
'unique_cadastral_numbers': len(self.get_unique_cadastral_numbers())
}
def __str__(self):
stats = self.get_statistics()
return (f"FileSeeker: {stats['total_xml_files']} XML файлов, "
f"{stats['unique_cadastral_numbers']} уникальных номеров")

56
test_modules.py Normal file
View File

@@ -0,0 +1,56 @@
"""
Тестовый скрипт для проверки работы новой модульной структуры
"""
def test_modules():
"""Тестирует импорт и базовую функциональность модулей"""
print("=== ТЕСТ МОДУЛЬНОЙ СТРУКТУРЫ ===\n")
try:
# Тест импорта моделей
print("1. Тестирование моделей...")
from models.cadastral_record import CadastralRecord
from models.xml_file_info import XMLFileInfo
# Создаем тестовую запись
record = CadastralRecord("74:34:0400003:1234", "test.xml")
print(f" ✅ CadastralRecord создан: {record}")
# Тест сервисов
print("\n2. Тестирование сервисов...")
from services.file_seeker import FileSeeker
from services.excel_processor import ExcelProcessor
# Создаем FileSeeker с тестовой директорией
import tempfile
temp_dir = tempfile.mkdtemp()
seeker = FileSeeker(temp_dir)
print(f" ✅ FileSeeker создан: {seeker}")
# Тест статистики
stats = seeker.get_statistics()
print(f" ✅ Статистика получена: {stats}")
print("\n3. Тестирование Excel процессора...")
# Создаем фиктивный Excel процессор
excel_path = "test.xlsx" # Не существует, но это OK для теста импорта
try:
excel_processor = ExcelProcessor(excel_path)
print(f" ✅ ExcelProcessor создан для: {excel_path}")
except Exception as e:
print(f" ⚠️ ExcelProcessor создан (ожидаемая ошибка файла): {type(e).__name__}")
print("\n🎉 ВСЕ МОДУЛИ УСПЕШНО ИМПОРТИРОВАНЫ И ПРОТЕСТИРОВАНЫ!")
return True
except ImportError as e:
print(f"❌ Ошибка импорта: {e}")
return False
except Exception as e:
print(f"❌ Неожиданная ошибка: {e}")
return False
if __name__ == "__main__":
test_modules()

287
uv.lock generated
View File

@@ -2,16 +2,50 @@ version = 1
revision = 2 revision = 2
requires-python = ">=3.12" requires-python = ">=3.12"
[[package]]
name = "altgraph"
version = "0.17.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/de/a8/7145824cf0b9e3c28046520480f207df47e927df83aa9555fb47f8505922/altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", size = 48418, upload-time = "2023-09-25T09:04:52.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212, upload-time = "2023-09-25T09:04:50.691Z" },
]
[[package]]
name = "et-xmlfile"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
]
[[package]] [[package]]
name = "kadastr14" name = "kadastr14"
version = "0.1.0" version = "2.0.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "lxml" }, { name = "lxml" },
{ name = "openpyxl" },
{ name = "pandas" },
{ name = "pathlib2" },
]
[package.dev-dependencies]
dev = [
{ name = "pyinstaller" },
] ]
[package.metadata] [package.metadata]
requires-dist = [{ name = "lxml", specifier = ">=6.0.0" }] requires-dist = [
{ name = "lxml", specifier = ">=6.0.0" },
{ name = "openpyxl", specifier = ">=3.1.5" },
{ name = "pandas", specifier = ">=2.3.1" },
{ name = "pathlib2", specifier = ">=2.3.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "pyinstaller", specifier = ">=6.15.0" }]
[[package]] [[package]]
name = "lxml" name = "lxml"
@@ -52,3 +86,252 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" }, { url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" },
{ url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" }, { url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" },
] ]
[[package]]
name = "macholib"
version = "1.16.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/ee/af1a3842bdd5902ce133bd246eb7ffd4375c38642aeb5dc0ae3a0329dfa2/macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30", size = 59309, upload-time = "2023-09-25T09:10:16.155Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094, upload-time = "2023-09-25T09:10:14.188Z" },
]
[[package]]
name = "numpy"
version = "2.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" },
{ url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" },
{ url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" },
{ url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" },
{ url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" },
{ url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" },
{ url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" },
{ url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" },
{ url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" },
{ url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" },
{ url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" },
{ url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" },
{ url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" },
{ url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" },
{ url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" },
{ url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" },
{ url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" },
{ url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" },
{ url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" },
{ url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" },
{ url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" },
{ url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" },
{ url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" },
{ url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" },
{ url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" },
{ url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" },
{ url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" },
{ url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" },
{ url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" },
{ url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" },
{ url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" },
{ url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" },
{ url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" },
{ url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" },
{ url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" },
{ url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" },
{ url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" },
{ url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" },
{ url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" },
{ url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" },
{ url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" },
{ url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" },
{ url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" },
{ url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" },
{ url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" },
{ url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" },
{ url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" },
{ url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" },
{ url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" },
{ url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" },
{ url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" },
{ url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" },
{ url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" },
{ url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" },
]
[[package]]
name = "openpyxl"
version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "et-xmlfile" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
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" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pandas"
version = "2.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" },
{ url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" },
{ url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" },
{ url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" },
{ url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" },
{ url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" },
{ url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" },
{ url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" },
{ url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" },
{ url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" },
{ url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" },
{ url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" },
{ url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" },
{ url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" },
{ url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" },
{ url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" },
{ url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" },
{ url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" },
{ url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" },
]
[[package]]
name = "pathlib2"
version = "2.3.7.post1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/31/51/99caf463dc7c18eb18dad1fffe465a3cf3ee50ac3d1dccbd1781336fe9c7/pathlib2-2.3.7.post1.tar.gz", hash = "sha256:9fe0edad898b83c0c3e199c842b27ed216645d2e177757b2dd67384d4113c641", size = 211190, upload-time = "2022-02-10T18:01:09.996Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/09/eb/4af4bcd5b8731366b676192675221c5324394a580dfae469d498313b5c4a/pathlib2-2.3.7.post1-py2.py3-none-any.whl", hash = "sha256:5266a0fd000452f1b3467d782f079a4343c63aaa119221fbdc4e39577489ca5b", size = 18027, upload-time = "2022-02-10T18:01:07.751Z" },
]
[[package]]
name = "pefile"
version = "2023.2.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/78/c5/3b3c62223f72e2360737fd2a57c30e5b2adecd85e70276879609a7403334/pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", size = 74854, upload-time = "2023-02-07T12:23:55.958Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791, upload-time = "2023-02-07T12:28:36.678Z" },
]
[[package]]
name = "pyinstaller"
version = "6.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
{ name = "macholib", marker = "sys_platform == 'darwin'" },
{ name = "packaging" },
{ name = "pefile", marker = "sys_platform == 'win32'" },
{ name = "pyinstaller-hooks-contrib" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/17/b2bb4de22650adbeef401fa82a1b43028976547a8728602e4d29735b455e/pyinstaller-6.15.0.tar.gz", hash = "sha256:a48fc4644ee4aa2aa2a35e7b51f496f8fbd7eecf6a2150646bbf1613ad07bc2d", size = 4331521, upload-time = "2025-08-03T18:33:35.709Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/dd/d5c8a127446adda954f68ea7fac22772f7ab8656ad4b06df396d82574ca9/pyinstaller-6.15.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:9f00c71c40148cd1e61695b2c6f1e086693d3bcf9bfa22ab513aa4254c3b966f", size = 1016981, upload-time = "2025-08-03T18:31:52.034Z" },
{ url = "https://files.pythonhosted.org/packages/2d/2a/7b50593b419db43e48d9bdeebaac0ff92a5fe035f3c30f87ca3e1650d7e2/pyinstaller-6.15.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:cbcc8eb77320c60722030ac875883b564e00768fe3ff1721c7ba3ad0e0a277e9", size = 726337, upload-time = "2025-08-03T18:31:57.592Z" },
{ url = "https://files.pythonhosted.org/packages/77/83/7f498fba0154c57eb5fc93eb9680a2dbadb9f780a3389fb85b8d79683378/pyinstaller-6.15.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c33e6302bc53db2df1104ed5566bd980b3e0ee7f18416a6e3caa908c12a54542", size = 737539, upload-time = "2025-08-03T18:32:02.221Z" },
{ url = "https://files.pythonhosted.org/packages/09/d6/e4477feab7c8379fb49e7ec95c82d0a69ad88f6ccc247f76bef3cb0e3432/pyinstaller-6.15.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:eb902d0fed3bb1f8b7190dc4df5c11f3b59505767e0d56d1ed782b853938bbf3", size = 735426, upload-time = "2025-08-03T18:32:06.485Z" },
{ url = "https://files.pythonhosted.org/packages/32/7e/ff25648276f15e2e77fc563d36d8cfcd917e077bf2a172420df3588601b4/pyinstaller-6.15.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b4df862adae7cf1f08eff53c43ace283822447f7f528f72e4f94749062712f15", size = 732210, upload-time = "2025-08-03T18:32:21.667Z" },
{ url = "https://files.pythonhosted.org/packages/db/3d/267a7dddd0647de95d260780050ccd8228ab29d2b9edea54ed1f56800967/pyinstaller-6.15.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b9ebf16ed0f99016ae8ae5746dee4cb244848a12941539e62ce2eea1df5a3f95", size = 732194, upload-time = "2025-08-03T18:32:29.536Z" },
{ url = "https://files.pythonhosted.org/packages/4d/61/962b2eb79ef225233e2d6e04600e998935328011dfb2fa775b1dd16b943a/pyinstaller-6.15.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:22193489e6a22435417103f61e7950363bba600ef36ec3ab1487303668c81092", size = 731256, upload-time = "2025-08-03T18:32:36.069Z" },
{ url = "https://files.pythonhosted.org/packages/67/5e/4e20e1c0e5791b09b69bef3ac921fd0cd25551b56879324ad999b92fa045/pyinstaller-6.15.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:18f743069849dbaee3e10900385f35795a5743eabab55e99dcc42f204e40a0db", size = 731148, upload-time = "2025-08-03T18:32:41.269Z" },
{ url = "https://files.pythonhosted.org/packages/88/31/28956c534991f289e2f981c715730b6241e75dc6295737a8cbd050a0cc8c/pyinstaller-6.15.0-py3-none-win32.whl", hash = "sha256:60da8f1b5071766b45c0f607d8bc3d7e59ba2c3b262d08f2e4066ba65f3544a2", size = 1312297, upload-time = "2025-08-03T18:32:50.572Z" },
{ url = "https://files.pythonhosted.org/packages/09/ab/6a45186c7f8e34c422faecd72580116a67d068158c57faa2d2f6d01faa7f/pyinstaller-6.15.0-py3-none-win_amd64.whl", hash = "sha256:cbea297e16eeda30b41c300d6ec2fd2abea4dbd8d8a32650eeec36431c94fcd9", size = 1373091, upload-time = "2025-08-03T18:32:58.133Z" },
{ url = "https://files.pythonhosted.org/packages/5b/86/72159af032b9db36f2470a3b085f79277ec1c38e7e48f8c5dc1ed16dc4e1/pyinstaller-6.15.0-py3-none-win_arm64.whl", hash = "sha256:f43c035621742cf2d19b84308c60e4e44e72c94786d176b8f6adcde351b5bd98", size = 1314305, upload-time = "2025-08-03T18:33:05.557Z" },
]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2025.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/71/d6/e5b378b7d4add8c879295c531309b0320e9c07a70458665d091760ffdc87/pyinstaller_hooks_contrib-2025.8.tar.gz", hash = "sha256:3402ad41dfe9b5110af134422e37fc5d421ba342c6cb980bd67cb30b7415641c", size = 164214, upload-time = "2025-07-27T16:37:31.943Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/34/1d973d0dae849683e53fbcda84443ce016f315e6f4dc7605ede4f56a28c3/pyinstaller_hooks_contrib-2025.8-py3-none-any.whl", hash = "sha256:8d0b8cfa0cb689a619294ae200497374234bd4e3994b3ace2a4442274c899064", size = 442346, upload-time = "2025-07-27T16:37:30.268Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
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 = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]
[[package]]
name = "setuptools"
version = "80.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
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" },
]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]