diff --git a/.gitignore b/.gitignore index e089228..ab7e7b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ *.pyc __pycache__/ -kadastr14_temp/ \ No newline at end of file +kadastr14_temp/ +найтийденные файлы/ +results/ +.venv/ +venv/ +build/ +*.xml +build_32bit/ +dist_32bit/ +dist/ +testset/ \ No newline at end of file diff --git a/BUILD_INSTRUCTIONS.md b/BUILD_INSTRUCTIONS.md new file mode 100644 index 0000000..1ad1458 --- /dev/null +++ b/BUILD_INSTRUCTIONS.md @@ -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 + +## Тестирование +Перед выпуском обязательно протестируйте на целевой системе! diff --git a/KadastrParser.spec b/KadastrParser.spec new file mode 100644 index 0000000..22f9580 --- /dev/null +++ b/KadastrParser.spec @@ -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', +) diff --git a/KadastrParser_32bit.spec b/KadastrParser_32bit.spec new file mode 100644 index 0000000..1fe7614 --- /dev/null +++ b/KadastrParser_32bit.spec @@ -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', +) diff --git a/VERSIONS_INFO.md b/VERSIONS_INFO.md new file mode 100644 index 0000000..2625fcb --- /dev/null +++ b/VERSIONS_INFO.md @@ -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` - он гарантированно будет работать! diff --git a/build_32bit.py b/build_32bit.py new file mode 100644 index 0000000..f19691a --- /dev/null +++ b/build_32bit.py @@ -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() diff --git a/build_32bit_final.py b/build_32bit_final.py new file mode 100644 index 0000000..04e6e9c --- /dev/null +++ b/build_32bit_final.py @@ -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) diff --git a/build_script.py b/build_script.py new file mode 100644 index 0000000..e8affbd --- /dev/null +++ b/build_script.py @@ -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() diff --git a/cadastral_record.py b/cadastral_record.py new file mode 100644 index 0000000..6fcbe21 --- /dev/null +++ b/cadastral_record.py @@ -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__() diff --git a/demo_structure.py b/demo_structure.py new file mode 100644 index 0000000..b8a7265 --- /dev/null +++ b/demo_structure.py @@ -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() diff --git a/excel_processor.py b/excel_processor.py new file mode 100644 index 0000000..44a7bea --- /dev/null +++ b/excel_processor.py @@ -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 diff --git a/main.py b/main.py index 7384164..5e4472f 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,403 @@ import tkinter as tk -from tkinter import filedialog - +from tkinter import filedialog, messagebox, ttk 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(): - 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.title("Parser12") +root.title("Parser14") font = ("Arial", 12) root.configure(bg="#B85C38") -options = [""] +root.geometry("600x500") + +# Устанавливаем минимальный размер окна +root.minsize(500, 250) #путь для сохранения файлов def open_directory(event, entry): directory_path = filedialog.askdirectory() - entry.delete(0, tk.END) - entry.insert(0, directory_path) + if 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.pack(anchor="w") -xml_entry = tk.Entry(root, font=font, width=70) -xml_entry.pack(anchor="w") +xml_label = tk.Label(main_frame, text="📁 Директория с XML файлами:", font=font, bg="#B85C38", fg="white") +xml_label.pack(anchor="w", pady=(0, 5)) +xml_entry = tk.Entry(main_frame, font=font, width=70) +xml_entry.pack(anchor="w", fill=tk.X, pady=(0, 15)) xml_entry.bind("", lambda event: open_directory(event, xml_entry)) -directory_label = tk.Label(root, text="Выберите директорию для сохранения:", font=font) -directory_label.pack(anchor="w") -directory_entry = tk.Entry(root, font=font, width=70) -directory_entry.pack(anchor="w") -directory_entry.bind("", lambda event: open_directory(event, directory_entry)) +# Поле для Excel файла (опционально) +excel_label = tk.Label(main_frame, text="📊 Excel файл со списком номеров (опционально):", font=font, bg="#B85C38", fg="white") +excel_label.pack(anchor="w", pady=(0, 5)) +excel_entry = tk.Entry(main_frame, font=font, width=70) +excel_entry.pack(anchor="w", fill=tk.X, pady=(0, 15)) +excel_entry.bind("", lambda event: open_excel_file(event, excel_entry)) -# кнопка -empty_button = tk.Button(root, text="->", command=Pars, font=font) -empty_button.pack(side="right", padx=10, pady=10) +# Информация о месте сохранения +info_label = tk.Label(main_frame, + 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() \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..18049a1 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1 @@ +# Модели данных diff --git a/models/cadastral_record.py b/models/cadastral_record.py new file mode 100644 index 0000000..31a8a1b --- /dev/null +++ b/models/cadastral_record.py @@ -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}')" diff --git a/models/xml_file_info.py b/models/xml_file_info.py new file mode 100644 index 0000000..2b16b5f --- /dev/null +++ b/models/xml_file_info.py @@ -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)}" diff --git a/pyproject.toml b/pyproject.toml index 31cc6b2..9eabadc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,20 @@ [project] name = "kadastr14" -version = "0.1.0" -description = "Add your description here" +version = "2.0.0" +description = "Парсер кадастровых номеров из XML файлов ЕГРН" readme = "README.md" -requires-python = ">=3.12" +authors = [ + {name = "Developer", email = "dev@example.com"} +] dependencies = [ "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", ] diff --git a/seekers.py b/seekers.py deleted file mode 100644 index 07063a4..0000000 --- a/seekers.py +++ /dev/null @@ -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)}") diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..7f43945 --- /dev/null +++ b/services/__init__.py @@ -0,0 +1 @@ +# Сервисы diff --git a/services/excel_processor.py b/services/excel_processor.py new file mode 100644 index 0000000..6f7038a --- /dev/null +++ b/services/excel_processor.py @@ -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) + } diff --git a/services/file_seeker.py b/services/file_seeker.py new file mode 100644 index 0000000..aefa0a9 --- /dev/null +++ b/services/file_seeker.py @@ -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']} уникальных номеров") diff --git a/test_modules.py b/test_modules.py new file mode 100644 index 0000000..1e14c16 --- /dev/null +++ b/test_modules.py @@ -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() diff --git a/uv.lock b/uv.lock index 124028f..bfe0ee5 100644 --- a/uv.lock +++ b/uv.lock @@ -2,16 +2,50 @@ version = 1 revision = 2 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]] name = "kadastr14" -version = "0.1.0" +version = "2.0.0" source = { virtual = "." } dependencies = [ { name = "lxml" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "pathlib2" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pyinstaller" }, ] [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]] 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/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" }, +]