diff --git a/.gitignore b/.gitignore index 2cca422..3b6d60c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -data/ +data +.streamlit + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -20,9 +22,19 @@ lib64/ parts/ sdist/ var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec # Installer logs pip-log.txt @@ -38,15 +50,79 @@ htmlcov/ nosetests.xml coverage.xml *.cover +*.py,cover .hypothesis/ .pytest_cache/ +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + # Jupyter Notebook .ipynb_checkpoints +# IPython +profile_default/ +ipython_config.py + # pyenv .python-version +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + # mypy .mypy_cache/ .dmypy.json @@ -55,36 +131,23 @@ dmypy.json # Pyre type checker .pyre/ -# VS Code +# IDE .vscode/ - -# PyCharm .idea/ +*.swp +*.swo +*~ -# Local envs -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# MacOS +# OS .DS_Store - -# Windows Thumbs.db -ehthumbs.db -Desktop.ini -# MinIO test data +# Project specific +data/ +*.zip +*.xlsx +*.xls +*.xlsm + +# MinIO data directory minio_data/ -minio_test/ -minio/ - -# Logs -*.log - -# Streamlit cache -.streamlit/ diff --git a/python_parser/Procfile b/Procfile similarity index 100% rename from python_parser/Procfile rename to Procfile diff --git a/README.md b/README.md new file mode 100644 index 0000000..893b300 --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +# 🚀 NIN Excel Parsers API - Полная система + +Полноценная система для парсинга Excel отчетов нефтеперерабатывающих заводов (НПЗ) с использованием FastAPI, MinIO и Streamlit. + +## 🏗️ Архитектура проекта + +Проект состоит из **двух изолированных пакетов**: + +- **`python_parser/`** - FastAPI сервер + парсеры Excel +- **`streamlit_app/`** - Веб-интерфейс для демонстрации API + +## 🚀 Быстрый запуск + +### **Вариант 1: Все сервисы в Docker (рекомендуется)** +```bash +# Запуск всех сервисов: MinIO + FastAPI + Streamlit +docker-compose up -d + +# Доступ: +# - MinIO Console: http://localhost:9001 +# - FastAPI: http://localhost:8000 +# - Streamlit: http://localhost:8501 +# - API Docs: http://localhost:8000/docs +``` + +### **Вариант 2: Только MinIO в Docker + сервисы локально** +```bash +# Запуск MinIO в Docker +docker-compose up -d minio + +# Запуск FastAPI локально +cd python_parser +python run_dev.py + +# В отдельном терминале - Streamlit +cd streamlit_app +streamlit run app.py +``` + +### **Вариант 3: Только MinIO в Docker** +```bash +# Запуск только MinIO +docker-compose up -d minio +``` + +## 📋 Описание сервисов + +- **MinIO** (порт 9000-9001): S3-совместимое хранилище для данных +- **FastAPI** (порт 8000): API сервер для парсинга Excel файлов +- **Streamlit** (порт 8501): Веб-интерфейс для демонстрации API + +## 📁 Структура проекта + +``` +python_parser_cf/ # Корень проекта +├── python_parser/ # Пакет FastAPI + парсеры +│ ├── app/ # FastAPI приложение +│ │ ├── main.py # Основной файл приложения +│ │ └── schemas/ # Pydantic схемы +│ ├── core/ # Бизнес-логика +│ │ ├── models.py # Модели данных +│ │ ├── ports.py # Интерфейсы (порты) +│ │ └── services.py # Сервисы +│ ├── adapters/ # Адаптеры для внешних систем +│ │ ├── storage.py # MinIO адаптер +│ │ └── parsers/ # Парсеры Excel файлов +│ ├── data/ # Тестовые данные +│ ├── Dockerfile # Docker образ для FastAPI +│ ├── requirements.txt # Зависимости FastAPI +│ └── run_dev.py # Запуск FastAPI локально +├── streamlit_app/ # Пакет Streamlit +│ ├── app.py # Основное Streamlit приложение +│ ├── requirements.txt # Зависимости Streamlit +│ ├── Dockerfile # Docker образ для Streamlit +│ ├── .streamlit/ # Конфигурация Streamlit +│ │ └── config.toml # Настройки +│ └── README.md # Документация Streamlit +├── docker-compose.yml # Docker Compose конфигурация +├── .gitignore # Git исключения +└── README.md # Общая документация +``` + +## 🔍 Доступные эндпоинты + +- **GET /** - Информация об API +- **GET /docs** - Swagger документация +- **GET /parsers** - Список доступных парсеров +- **GET /parsers/{parser_name}/getters** - Информация о геттерах парсера +- **POST /svodka_pm/upload-zip** - Загрузка сводок ПМ +- **POST /svodka_ca/upload** - Загрузка сводок ЦА +- **POST /monitoring_fuel/upload-zip** - Загрузка мониторинга топлива +- **POST /svodka_pm/get_data** - Получение данных сводок ПМ +- **POST /svodka_ca/get_data** - Получение данных сводок ЦА +- **POST /monitoring_fuel/get_data** - Получение данных мониторинга топлива + +## 📊 Поддерживаемые типы отчетов + +1. **svodka_pm** - Сводки по переработке нефти (ПМ) + - Геттеры: `single_og`, `total_ogs` +2. **svodka_ca** - Сводки по переработке нефти (ЦА) + - Геттеры: `get_data` +3. **monitoring_fuel** - Мониторинг топлива + - Геттеры: `total_by_columns`, `month_by_code` + +## 🏗️ Архитектура + +Проект использует **Hexagonal Architecture (Ports and Adapters)**: + +- **Порты (Ports)**: Интерфейсы для бизнес-логики +- **Адаптеры (Adapters)**: Реализации для внешних систем +- **Сервисы (Services)**: Бизнес-логика приложения + +### Система геттеров парсеров + +Каждый парсер может иметь несколько методов получения данных (геттеров): +- Регистрация геттеров в словаре с метаданными +- Валидация параметров для каждого геттера +- Единый интерфейс `get_value(getter_name, params)` + +## 🐳 Docker + +### Сборка образов: +```bash +# FastAPI +docker build -t nin-fastapi ./python_parser + +# Streamlit +docker build -t nin-streamlit ./streamlit_app +``` + +### Запуск отдельных сервисов: +```bash +# Только MinIO +docker-compose up -d minio + +# MinIO + FastAPI +docker-compose up -d minio fastapi + +# Все сервисы +docker-compose up -d +``` + +## 🛑 Остановка + +### Остановка Docker сервисов: +```bash +# Все сервисы +docker-compose down + +# Только MinIO +docker-compose stop minio +``` + +### Остановка локальных сервисов: +```bash +# Нажмите Ctrl+C в терминале с FastAPI/Streamlit +``` + +## 🔧 Разработка + +### Добавление нового парсера: + +1. Создайте файл в `python_parser/adapters/parsers/` +2. Реализуйте интерфейс `ParserPort` +3. Добавьте в `python_parser/core/services.py` +4. Создайте схемы в `python_parser/app/schemas/` +5. Добавьте эндпоинты в `python_parser/app/main.py` + +### Тестирование: + +```bash +# Запуск тестов +cd python_parser +pytest + +# Запуск с покрытием +pytest --cov=. +``` + +## 📝 Лицензия + +Проект разработан для внутреннего использования НИН. \ No newline at end of file diff --git a/python_parser/check_services.py b/check_services.py similarity index 94% rename from python_parser/check_services.py rename to check_services.py index e03fd9f..05cc683 100644 --- a/python_parser/check_services.py +++ b/check_services.py @@ -170,16 +170,11 @@ def main(): if not port_8000_ok: print("\n🔧 РЕШЕНИЕ: Запустите FastAPI сервер") - print("python run_dev.py") + print("docker-compose up -d fastapi") if not port_8501_ok: print("\n🔧 РЕШЕНИЕ: Запустите Streamlit") - print("python run_streamlit.py") - - print("\n🚀 Для автоматического запуска используйте:") - print("python start_demo.py") - print("\n🔍 Для пошагового запуска используйте:") - print("python run_manual.py") + print("docker-compose up -d streamlit") if __name__ == "__main__": main() \ No newline at end of file diff --git a/create_test_excel.py b/create_test_excel.py new file mode 100644 index 0000000..93553b5 --- /dev/null +++ b/create_test_excel.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +""" +Создание тестового Excel файла для тестирования API +""" + +import pandas as pd +import numpy as np + +def create_test_excel(): + """Создание тестового Excel файла""" + + # Создаем тестовые данные + data = { + 'name': ['Установка 1', 'Установка 2', 'Установка 3'], + 'normativ': [100, 200, 300], + 'total': [95, 195, 295], + 'total_1': [90, 190, 290] + } + + df = pd.DataFrame(data) + + # Сохраняем в Excel + filename = 'test_file.xlsx' + with pd.ExcelWriter(filename, engine='openpyxl') as writer: + df.to_excel(writer, sheet_name='Мониторинг потребления', index=False) + + print(f"✅ Тестовый файл создан: {filename}") + print(f"📊 Содержимое: {len(df)} строк, {len(df.columns)} столбцов") + print(f"📋 Столбцы: {list(df.columns)}") + + return filename + +if __name__ == "__main__": + create_test_excel() \ No newline at end of file diff --git a/python_parser/docker-compose.yml b/docker-compose.yml similarity index 89% rename from python_parser/docker-compose.yml rename to docker-compose.yml index 572b64e..b400b34 100644 --- a/python_parser/docker-compose.yml +++ b/docker-compose.yml @@ -10,11 +10,11 @@ services: MINIO_ROOT_PASSWORD: minioadmin command: server /data --console-address ":9001" volumes: - - minio_data:/data + - ./minio_data:/data restart: unless-stopped fastapi: - build: . + build: ./python_parser container_name: svodka_fastapi ports: - "8000:8000" @@ -35,9 +35,7 @@ services: - "8501:8501" environment: - API_BASE_URL=http://fastapi:8000 + - DOCKER_ENV=true depends_on: - fastapi - restart: unless-stopped - -volumes: - minio_data: \ No newline at end of file + restart: unless-stopped \ No newline at end of file diff --git a/python_parser/manifest.yml b/manifest.yml similarity index 100% rename from python_parser/manifest.yml rename to manifest.yml diff --git a/python_parser/.streamlit/config.toml b/python_parser/.streamlit/config.toml deleted file mode 100644 index 84e0551..0000000 --- a/python_parser/.streamlit/config.toml +++ /dev/null @@ -1,28 +0,0 @@ -[server] -port = 8501 -address = "localhost" -headless = false -enableCORS = false -enableXsrfProtection = false - -[browser] -gatherUsageStats = false -serverAddress = "localhost" -serverPort = 8501 - -[theme] -primaryColor = "#FF4B4B" -backgroundColor = "#FFFFFF" -secondaryBackgroundColor = "#F0F2F6" -textColor = "#262730" -font = "sans serif" - -[client] -showErrorDetails = true -caching = true -displayEnabled = true - -[runner] -magicEnabled = true -installTracer = false -fixMatplotlib = true \ No newline at end of file diff --git a/python_parser/QUICK_START.md b/python_parser/QUICK_START.md deleted file mode 100644 index d81b67f..0000000 --- a/python_parser/QUICK_START.md +++ /dev/null @@ -1,66 +0,0 @@ -# 🚀 Быстрый старт NIN Excel Parsers API - -## 🐳 Запуск через Docker (рекомендуется) - -### Вариант 1: MinIO + FastAPI в Docker -```bash -# Запуск всех сервисов -docker-compose up -d --build - -# Проверка -curl http://localhost:8000 -curl http://localhost:9001 -``` - -### Вариант 2: Только MinIO в Docker -```bash -# Запуск только MinIO -docker-compose up -d minio - -# Проверка -curl http://localhost:9001 -``` - -## 🖥️ Запуск FastAPI локально - -```bash -# Если MinIO в Docker -python run_dev.py - -# Проверка -curl http://localhost:8000 -``` - -## 📊 Запуск Streamlit - -```bash -# В отдельном терминале -python run_streamlit.py -``` - -## 🌐 Доступные URL - -- **FastAPI API**: http://localhost:8000 -- **API документация**: http://localhost:8000/docs -- **MinIO консоль**: http://localhost:9001 -- **Streamlit интерфейс**: http://localhost:8501 - -## 🛑 Остановка - -```bash -# Остановка Docker -docker-compose down - -# Остановка Streamlit -# Ctrl+C в терминале -``` - -## 🔧 Диагностика - -```bash -# Проверка состояния -python check_services.py - -# Просмотр логов Docker -docker-compose logs -``` \ No newline at end of file diff --git a/python_parser/README.md b/python_parser/README.md index 8e9ae99..8941643 100644 --- a/python_parser/README.md +++ b/python_parser/README.md @@ -1,63 +1,28 @@ -# NIN Excel Parsers API +# 📊 Python Parser - FastAPI + Парсеры Excel -API для парсинга Excel отчетов нефтеперерабатывающих заводов (НПЗ) с использованием FastAPI и MinIO для хранения данных. +Пакет FastAPI сервера и парсеров Excel для нефтеперерабатывающих заводов. ## 🚀 Быстрый запуск -### **Вариант 1: Все сервисы в Docker (рекомендуется)** +### **Локально:** ```bash -# Запуск всех сервисов: MinIO + FastAPI + Streamlit -docker-compose up -d +# Установка зависимостей +pip install -r requirements.txt -# Доступ: -# - MinIO Console: http://localhost:9001 -# - FastAPI: http://localhost:8000 -# - Streamlit: http://localhost:8501 -# - API Docs: http://localhost:8000/docs -``` - -### **Вариант 2: Только MinIO в Docker + FastAPI локально** -```bash -# Запуск MinIO в Docker -docker-compose up -d minio - -# Запуск FastAPI локально +# Запуск FastAPI сервера python run_dev.py - -# В отдельном терминале запуск Streamlit -cd streamlit_app -streamlit run app.py ``` -### **Вариант 3: Только MinIO в Docker** +### **В Docker:** ```bash -# Запуск только MinIO -docker-compose up -d minio +# Сборка образа +docker build -t nin-fastapi . + +# Запуск контейнера +docker run -p 8000:8000 nin-fastapi ``` -## 📋 Описание сервисов - -- **MinIO** (порт 9000-9001): S3-совместимое хранилище для данных -- **FastAPI** (порт 8000): API сервер для парсинга Excel файлов -- **Streamlit** (порт 8501): Веб-интерфейс для демонстрации API - -## 🛑 Остановка - -### Остановка Docker сервисов: -```bash -# Все сервисы -docker-compose down - -# Только MinIO -docker-compose stop minio -``` - -### Остановка локальных сервисов: -```bash -# Нажмите Ctrl+C в терминале с FastAPI/Streamlit -``` - -## 📁 Структура проекта +## 📁 Структура пакета ``` python_parser/ @@ -71,18 +36,13 @@ python_parser/ ├── adapters/ # Адаптеры для внешних систем │ ├── storage.py # MinIO адаптер │ └── parsers/ # Парсеры Excel файлов -├── streamlit_app/ # Изолированный Streamlit пакет -│ ├── app.py # Основное Streamlit приложение -│ ├── requirements.txt # Зависимости Streamlit -│ ├── Dockerfile # Docker образ для Streamlit -│ └── .streamlit/ # Конфигурация Streamlit ├── data/ # Тестовые данные -├── docker-compose.yml # Docker Compose конфигурация ├── Dockerfile # Docker образ для FastAPI -└── run_dev.py # Запуск FastAPI локально +├── requirements.txt # Зависимости Python +└── run_dev.py # Запуск FastAPI локально ``` -## 🔍 Доступные эндпоинты +## 🔍 Основные эндпоинты - **GET /** - Информация об API - **GET /docs** - Swagger документация @@ -95,7 +55,7 @@ python_parser/ - **POST /svodka_ca/get_data** - Получение данных сводок ЦА - **POST /monitoring_fuel/get_data** - Получение данных мониторинга топлива -## 📊 Поддерживаемые типы отчетов +## 📊 Поддерживаемые парсеры 1. **svodka_pm** - Сводки по переработке нефти (ПМ) - Геттеры: `single_og`, `total_ogs` @@ -106,7 +66,7 @@ python_parser/ ## 🏗️ Архитектура -Проект использует **Hexagonal Architecture (Ports and Adapters)**: +Использует **Hexagonal Architecture (Ports and Adapters)**: - **Порты (Ports)**: Интерфейсы для бизнес-логики - **Адаптеры (Adapters)**: Реализации для внешних систем @@ -119,25 +79,26 @@ python_parser/ - Валидация параметров для каждого геттера - Единый интерфейс `get_value(getter_name, params)` -## 🐳 Docker +## 🔧 Разработка + +### Добавление нового парсера: + +1. Создайте файл в `adapters/parsers/` +2. Реализуйте интерфейс `ParserPort` +3. Добавьте в `core/services.py` +4. Создайте схемы в `app/schemas/` +5. Добавьте эндпоинты в `app/main.py` + +### Тестирование: -### Сборка образов: ```bash -# FastAPI -docker build -t nin-fastapi . +# Запуск тестов +pytest -# Streamlit -docker build -t nin-streamlit ./streamlit_app +# Запуск с покрытием +pytest --cov=. ``` -### Запуск отдельных сервисов: -```bash -# Только MinIO -docker-compose up -d minio +## 📝 Примечание -# MinIO + FastAPI -docker-compose up -d minio fastapi - -# Все сервисы -docker-compose up -d -``` \ No newline at end of file +Этот пакет является частью большей системы. Для полной документации и запуска всех сервисов см. README.md в корне проекта. \ No newline at end of file diff --git a/python_parser/README_STREAMLIT.md b/python_parser/README_STREAMLIT.md deleted file mode 100644 index d3b0d36..0000000 --- a/python_parser/README_STREAMLIT.md +++ /dev/null @@ -1,186 +0,0 @@ -# 🚀 Streamlit Demo для NIN Excel Parsers API - -## Описание - -Streamlit приложение для демонстрации работы всех API эндпоинтов NIN Excel Parsers. Предоставляет удобный веб-интерфейс для тестирования функциональности парсеров. - -## Возможности - -- 📤 **Загрузка файлов**: Загрузка ZIP архивов и Excel файлов -- 📊 **Сводки ПМ**: Работа с плановыми и фактическими данными -- 🏭 **Сводки СА**: Парсинг сводок центрального аппарата -- ⛽ **Мониторинг топлива**: Анализ данных по топливу -- 📱 **Адаптивный интерфейс**: Удобное использование на всех устройствах - -## Установка и запуск - -### 1. Установка зависимостей - -```bash -pip install -r requirements.txt -``` - -### 2. Запуск FastAPI сервера - -В одном терминале: -```bash -python run_dev.py -``` - -### 3. Запуск Streamlit приложения - -В другом терминале: -```bash -python run_streamlit.py -``` - -Или напрямую: -```bash -streamlit run streamlit_app.py -``` - -### 4. Открытие в браузере - -Приложение автоматически откроется по адресу: http://localhost:8501 - -## Конфигурация - -### Переменные окружения - -```bash -# URL API сервера -export API_BASE_URL="http://localhost:8000" - -# Порт Streamlit -export STREAMLIT_PORT="8501" - -# Хост Streamlit -export STREAMLIT_HOST="localhost" -``` - -### Настройки Streamlit - -Файл `.streamlit/config.toml` содержит настройки: -- Порт: 8501 -- Хост: localhost -- Тема: Кастомная цветовая схема -- Безопасность: Отключены CORS и XSRF для локальной разработки - -## Структура приложения - -### Вкладки - -1. **📤 Загрузка файлов** - - Загрузка сводок ПМ (ZIP) - - Загрузка мониторинга топлива (ZIP) - - Загрузка сводки СА (Excel) - -2. **📊 Сводки ПМ** - - Данные по одному ОГ - - Данные по всем ОГ - - Выбор кодов строк и столбцов - -3. **🏭 Сводки СА** - - Выбор режимов (план/факт/норматив) - - Выбор таблиц для анализа - -4. **⛽ Мониторинг топлива** - - Агрегация по колонкам - - Данные за конкретный месяц - -### Боковая панель - -- Информация о сервере (PID, CPU, память) -- Список доступных парсеров -- Статус подключения к API - -## Использование - -### 1. Загрузка файлов - -1. Выберите соответствующий тип файла -2. Нажмите "Загрузить" -3. Дождитесь подтверждения загрузки - -### 2. Получение данных - -1. Выберите нужные параметры (ОГ, коды, столбцы) -2. Нажмите "Получить данные" -3. Результат отобразится в JSON формате - -### 3. Мониторинг - -- Проверяйте статус API в верхней части -- Следите за логами операций -- Используйте индикаторы загрузки - -## Устранение неполадок - -### API недоступен - -```bash -# Проверьте, запущен ли FastAPI сервер -curl http://localhost:8000/ - -# Проверьте порт -netstat -an | grep 8000 -``` - -### Streamlit не запускается - -```bash -# Проверьте версию Python -python --version - -# Переустановите Streamlit -pip uninstall streamlit -pip install streamlit - -# Проверьте порт 8501 -netstat -an | grep 8501 -``` - -### Ошибки загрузки файлов - -- Убедитесь, что файл соответствует формату -- Проверьте размер файла (не более 100MB) -- Убедитесь, что MinIO запущен - -## Разработка - -### Добавление новых функций - -1. Создайте новую вкладку в `streamlit_app.py` -2. Добавьте соответствующие API вызовы -3. Обновите боковую панель при необходимости - -### Кастомизация темы - -Отредактируйте `.streamlit/config.toml`: -```toml -[theme] -primaryColor = "#FF4B4B" -backgroundColor = "#FFFFFF" -# ... другие цвета -``` - -### Добавление новых парсеров - -1. Создайте парсер в `adapters/parsers/` -2. Добавьте в `main.py` -3. Обновите Streamlit интерфейс - -## Безопасность - -⚠️ **Внимание**: Приложение настроено для локальной разработки -- CORS отключен -- XSRF защита отключена -- Не используйте в продакшене без дополнительной настройки - -## Поддержка - -При возникновении проблем: -1. Проверьте логи в терминале -2. Убедитесь, что все сервисы запущены -3. Проверьте конфигурацию -4. Обратитесь к документации API: http://localhost:8000/docs \ No newline at end of file diff --git a/python_parser/adapters/parsers/monitoring_fuel.py b/python_parser/adapters/parsers/monitoring_fuel.py index 4536b98..129a812 100644 --- a/python_parser/adapters/parsers/monitoring_fuel.py +++ b/python_parser/adapters/parsers/monitoring_fuel.py @@ -94,7 +94,8 @@ class MonitoringFuelParser(ParserPort): file_path, sheet_name=sheet, header=None, - nrows=max_rows + nrows=max_rows, + engine='openpyxl' ) # Ищем строку, где хотя бы в одном столбце встречается искомое значение @@ -116,7 +117,8 @@ class MonitoringFuelParser(ParserPort): sheet_name=sheet, header=header_num, usecols=None, - index_col=None + index_col=None, + engine='openpyxl' ) # === Удаление полностью пустых столбцов === diff --git a/python_parser/adapters/parsers/svodka_ca.py b/python_parser/adapters/parsers/svodka_ca.py index e557542..289e25b 100644 --- a/python_parser/adapters/parsers/svodka_ca.py +++ b/python_parser/adapters/parsers/svodka_ca.py @@ -44,6 +44,10 @@ class SvodkaCAParser(ParserPort): def parse_svodka_ca(self, file_path: str, params: dict) -> dict: """Парсинг сводки СА""" + # Получаем параметры из params + sheet_name = params.get('sheet_name', 0) # По умолчанию первый лист + inclusion_list = params.get('inclusion_list', {'ТиП', 'Топливо', 'Потери'}) + # === Извлечение и фильтрация === tables = self.extract_all_tables(file_path, sheet_name) @@ -150,8 +154,8 @@ class SvodkaCAParser(ParserPort): return None def extract_all_tables(self, file_path, sheet_name=0): - """Извлекает все таблицы из Excel файла""" - df = pd.read_excel(file_path, sheet_name=sheet_name, header=None) + """Извлечение всех таблиц из Excel файла""" + df = pd.read_excel(file_path, sheet_name=sheet_name, header=None, engine='openpyxl') df_filled = df.fillna('') df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True) diff --git a/python_parser/adapters/parsers/svodka_pm.py b/python_parser/adapters/parsers/svodka_pm.py index 5e913d1..008c2f9 100644 --- a/python_parser/adapters/parsers/svodka_pm.py +++ b/python_parser/adapters/parsers/svodka_pm.py @@ -70,7 +70,8 @@ class SvodkaPMParser(ParserPort): file, sheet_name=sheet, header=None, - nrows=max_rows + nrows=max_rows, + engine='openpyxl' ) # Ищем строку, где хотя бы в одном столбце встречается искомое значение @@ -94,6 +95,7 @@ class SvodkaPMParser(ParserPort): header=header_num, usecols=None, nrows=2, + engine='openpyxl' ) if df_probe.shape[0] == 0: @@ -115,7 +117,8 @@ class SvodkaPMParser(ParserPort): sheet_name=sheet, header=header_num, usecols=None, - index_col=None + index_col=None, + engine='openpyxl' ) if indicator_col_name not in df_full.columns: diff --git a/python_parser/app/main.py b/python_parser/app/main.py index e645660..578d06a 100644 --- a/python_parser/app/main.py +++ b/python_parser/app/main.py @@ -400,40 +400,40 @@ async def get_svodka_pm_total_ogs( raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") -# @app.post("/svodka_pm/get_data", tags=[SvodkaPMParser.name]) -# async def get_svodka_pm_data( -# request_data: dict -# ): -# report_service = get_report_service() -# """ -# Получение данных из отчета сводки факта СарНПЗ +@app.post("/svodka_pm/get_data", tags=[SvodkaPMParser.name]) +async def get_svodka_pm_data( + request_data: dict +): + report_service = get_report_service() + """ + Получение данных из отчета сводки факта СарНПЗ -# - indicator_id: ID индикатора -# - code: Код для поиска -# - search_value: Опциональное значение для поиска -# """ -# try: -# # Создаем запрос -# request = DataRequest( -# report_type='svodka_pm', -# get_params=request_data -# ) + - indicator_id: ID индикатора + - code: Код для поиска + - search_value: Опциональное значение для поиска + """ + try: + # Создаем запрос + request = DataRequest( + report_type='svodka_pm', + get_params=request_data + ) -# # Получаем данные -# result = report_service.get_data(request) + # Получаем данные + result = report_service.get_data(request) -# if result.success: -# return { -# "success": True, -# "data": result.data -# } -# else: -# raise HTTPException(status_code=404, detail=result.message) + if result.success: + return { + "success": True, + "data": result.data + } + else: + raise HTTPException(status_code=404, detail=result.message) -# except HTTPException: -# raise -# except Exception as e: -# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") @app.post("/svodka_ca/upload", tags=[SvodkaCAParser.name], @@ -610,38 +610,38 @@ async def get_svodka_ca_data( # raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") -# @app.post("/monitoring_fuel/get_data", tags=[MonitoringFuelParser.name]) -# async def get_monitoring_fuel_data( -# request_data: dict -# ): -# report_service = get_report_service() -# """ -# Получение данных из отчета мониторинга топлива +@app.post("/monitoring_fuel/get_data", tags=[MonitoringFuelParser.name]) +async def get_monitoring_fuel_data( + request_data: dict +): + report_service = get_report_service() + """ + Получение данных из отчета мониторинга топлива -# - column: Название колонки для агрегации (normativ, total, total_svod) -# """ -# try: -# # Создаем запрос -# request = DataRequest( -# report_type='monitoring_fuel', -# get_params=request_data -# ) + - column: Название колонки для агрегации (normativ, total, total_svod) + """ + try: + # Создаем запрос + request = DataRequest( + report_type='monitoring_fuel', + get_params=request_data + ) -# # Получаем данные -# result = report_service.get_data(request) + # Получаем данные + result = report_service.get_data(request) -# if result.success: -# return { -# "success": True, -# "data": result.data -# } -# else: -# raise HTTPException(status_code=404, detail=result.message) + if result.success: + return { + "success": True, + "data": result.data + } + else: + raise HTTPException(status_code=404, detail=result.message) -# except HTTPException: -# raise -# except Exception as e: -# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") # @app.post("/monitoring_fuel/upload_directory", tags=[MonitoringFuelParser.name]) diff --git a/python_parser/minio/.minio.sys/buckets/.bloomcycle.bin/xl.meta b/python_parser/minio/.minio.sys/buckets/.bloomcycle.bin/xl.meta deleted file mode 100644 index 60320f3..0000000 Binary files a/python_parser/minio/.minio.sys/buckets/.bloomcycle.bin/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/buckets/.heal/mrf/list.bin b/python_parser/minio/.minio.sys/buckets/.heal/mrf/list.bin deleted file mode 100644 index a356872..0000000 Binary files a/python_parser/minio/.minio.sys/buckets/.heal/mrf/list.bin and /dev/null differ diff --git a/python_parser/minio/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta b/python_parser/minio/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta deleted file mode 100644 index 0a02b26..0000000 Binary files a/python_parser/minio/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/buckets/.usage-cache.bin/xl.meta b/python_parser/minio/.minio.sys/buckets/.usage-cache.bin/xl.meta deleted file mode 100644 index 241fe0b..0000000 Binary files a/python_parser/minio/.minio.sys/buckets/.usage-cache.bin/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/buckets/.usage.json/xl.meta b/python_parser/minio/.minio.sys/buckets/.usage.json/xl.meta deleted file mode 100644 index 2bb41d0..0000000 Binary files a/python_parser/minio/.minio.sys/buckets/.usage.json/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/buckets/svodka-data/.metadata.bin/xl.meta b/python_parser/minio/.minio.sys/buckets/svodka-data/.metadata.bin/xl.meta deleted file mode 100644 index 5eef2e3..0000000 Binary files a/python_parser/minio/.minio.sys/buckets/svodka-data/.metadata.bin/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/buckets/svodka-data/.usage-cache.bin.bkp/xl.meta b/python_parser/minio/.minio.sys/buckets/svodka-data/.usage-cache.bin.bkp/xl.meta deleted file mode 100644 index 1f31843..0000000 Binary files a/python_parser/minio/.minio.sys/buckets/svodka-data/.usage-cache.bin.bkp/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/buckets/svodka-data/.usage-cache.bin/xl.meta b/python_parser/minio/.minio.sys/buckets/svodka-data/.usage-cache.bin/xl.meta deleted file mode 100644 index 0774939..0000000 Binary files a/python_parser/minio/.minio.sys/buckets/svodka-data/.usage-cache.bin/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/config/config.json/xl.meta b/python_parser/minio/.minio.sys/config/config.json/xl.meta deleted file mode 100644 index f075295..0000000 Binary files a/python_parser/minio/.minio.sys/config/config.json/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/config/iam/format.json/xl.meta b/python_parser/minio/.minio.sys/config/iam/format.json/xl.meta deleted file mode 100644 index 5eb932b..0000000 Binary files a/python_parser/minio/.minio.sys/config/iam/format.json/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/config/iam/sts/46KX4VILT0DATJ36SYGD/identity.json/xl.meta b/python_parser/minio/.minio.sys/config/iam/sts/46KX4VILT0DATJ36SYGD/identity.json/xl.meta deleted file mode 100644 index 57e8644..0000000 Binary files a/python_parser/minio/.minio.sys/config/iam/sts/46KX4VILT0DATJ36SYGD/identity.json/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/format.json b/python_parser/minio/.minio.sys/format.json deleted file mode 100644 index d9111c6..0000000 --- a/python_parser/minio/.minio.sys/format.json +++ /dev/null @@ -1 +0,0 @@ -{"version":"1","format":"xl-single","id":"29118f57-702e-4363-9a41-9f06655e449d","xl":{"version":"3","this":"195a90f4-fc26-46a8-b6d4-0b50b99b1342","sets":[["195a90f4-fc26-46a8-b6d4-0b50b99b1342"]],"distributionAlgo":"SIPMOD+PARITY"}} \ No newline at end of file diff --git a/python_parser/minio/.minio.sys/pool.bin/xl.meta b/python_parser/minio/.minio.sys/pool.bin/xl.meta deleted file mode 100644 index 794f4ef..0000000 Binary files a/python_parser/minio/.minio.sys/pool.bin/xl.meta and /dev/null differ diff --git a/python_parser/minio/.minio.sys/tmp/060ca61d-ef6c-4011-9a9f-d083313258e8 b/python_parser/minio/.minio.sys/tmp/060ca61d-ef6c-4011-9a9f-d083313258e8 deleted file mode 100644 index e36ee44..0000000 Binary files a/python_parser/minio/.minio.sys/tmp/060ca61d-ef6c-4011-9a9f-d083313258e8 and /dev/null differ diff --git a/python_parser/minio/svodka-data/nin_excel_data_monitoring_fuel/xl.meta b/python_parser/minio/svodka-data/nin_excel_data_monitoring_fuel/xl.meta deleted file mode 100644 index bd3d49b..0000000 Binary files a/python_parser/minio/svodka-data/nin_excel_data_monitoring_fuel/xl.meta and /dev/null differ diff --git a/python_parser/minio/svodka-data/nin_excel_data_svodka_ca/xl.meta b/python_parser/minio/svodka-data/nin_excel_data_svodka_ca/xl.meta deleted file mode 100644 index 428e851..0000000 Binary files a/python_parser/minio/svodka-data/nin_excel_data_svodka_ca/xl.meta and /dev/null differ diff --git a/python_parser/minio/svodka-data/nin_excel_data_svodka_pm/e78ade56-02ac-482f-b4c7-98b217fb7050/part.1 b/python_parser/minio/svodka-data/nin_excel_data_svodka_pm/e78ade56-02ac-482f-b4c7-98b217fb7050/part.1 deleted file mode 100644 index ebc7a06..0000000 Binary files a/python_parser/minio/svodka-data/nin_excel_data_svodka_pm/e78ade56-02ac-482f-b4c7-98b217fb7050/part.1 and /dev/null differ diff --git a/python_parser/minio/svodka-data/nin_excel_data_svodka_pm/e78ade56-02ac-482f-b4c7-98b217fb7050/part.2 b/python_parser/minio/svodka-data/nin_excel_data_svodka_pm/e78ade56-02ac-482f-b4c7-98b217fb7050/part.2 deleted file mode 100644 index 1cb0bc2..0000000 Binary files a/python_parser/minio/svodka-data/nin_excel_data_svodka_pm/e78ade56-02ac-482f-b4c7-98b217fb7050/part.2 and /dev/null differ diff --git a/python_parser/minio/svodka-data/nin_excel_data_svodka_pm/xl.meta b/python_parser/minio/svodka-data/nin_excel_data_svodka_pm/xl.meta deleted file mode 100644 index 4278033..0000000 Binary files a/python_parser/minio/svodka-data/nin_excel_data_svodka_pm/xl.meta and /dev/null differ diff --git a/python_parser/streamlit_app.py b/python_parser/streamlit_app.py deleted file mode 100644 index a11c130..0000000 --- a/python_parser/streamlit_app.py +++ /dev/null @@ -1,396 +0,0 @@ -import streamlit as st -import requests -import json -import pandas as pd -import io -import zipfile -from typing import Dict, Any -import os - -# Конфигурация страницы -st.set_page_config( - page_title="NIN Excel Parsers API Demo", - page_icon="📊", - layout="wide", - initial_sidebar_state="expanded" -) - -# Конфигурация API -API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8000") - -def check_api_health(): - """Проверка доступности API""" - try: - response = requests.get(f"{API_BASE_URL}/", timeout=5) - return response.status_code == 200 - except: - return False - -def get_available_parsers(): - """Получение списка доступных парсеров""" - try: - response = requests.get(f"{API_BASE_URL}/parsers") - if response.status_code == 200: - return response.json()["parsers"] - return [] - except: - return [] - -def get_server_info(): - """Получение информации о сервере""" - try: - response = requests.get(f"{API_BASE_URL}/server-info") - if response.status_code == 200: - return response.json() - return {} - except: - return {} - -def upload_file_to_api(endpoint: str, file_data: bytes, filename: str): - """Загрузка файла на API""" - try: - files = {"zip_file": (filename, file_data, "application/zip")} - response = requests.post(f"{API_BASE_URL}{endpoint}", files=files) - return response.json(), response.status_code - except Exception as e: - return {"error": str(e)}, 500 - -def make_api_request(endpoint: str, data: Dict[str, Any]): - """Выполнение API запроса""" - try: - response = requests.post(f"{API_BASE_URL}{endpoint}", json=data) - return response.json(), response.status_code - except Exception as e: - return {"error": str(e)}, 500 - -def main(): - st.title("🚀 NIN Excel Parsers API - Демонстрация") - st.markdown("---") - - # Проверка доступности API - if not check_api_health(): - st.error(f"❌ API недоступен по адресу {API_BASE_URL}") - st.info("Убедитесь, что FastAPI сервер запущен") - return - - st.success(f"✅ API доступен по адресу {API_BASE_URL}") - - # Боковая панель с информацией - with st.sidebar: - st.header("ℹ️ Информация") - - # Информация о сервере - server_info = get_server_info() - if server_info: - st.subheader("Сервер") - st.write(f"PID: {server_info.get('process_id', 'N/A')}") - st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}") - st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB") - - # Доступные парсеры - parsers = get_available_parsers() - if parsers: - st.subheader("Доступные парсеры") - for parser in parsers: - st.write(f"• {parser}") - - # Основные вкладки - по одной на каждый парсер - tab1, tab2, tab3 = st.tabs([ - "📊 Сводки ПМ", - "🏭 Сводки СА", - "⛽ Мониторинг топлива" - ]) - - # Вкладка 1: Сводки ПМ - полный функционал - with tab1: - st.header("📊 Сводки ПМ - Полный функционал") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_pm = st.file_uploader( - "Выберите ZIP архив со сводками ПМ", - type=['zip'], - key="pm_upload" - ) - - if uploaded_pm is not None: - if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"): - with st.spinner("Загружаю файл..."): - result, status = upload_file_to_api( - "/svodka_pm/upload-zip", - uploaded_pm.read(), - uploaded_pm.name - ) - - if status == 200: - st.success(f"✅ {result.get('message', 'Файл загружен')}") - st.info(f"ID объекта: {result.get('object_id', 'N/A')}") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Данные по одному ОГ") - - og_id = st.selectbox( - "Выберите ОГ", - ["SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV", - "NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK", - "NVNPO", "KLNPZ", "PurNP", "YANOS"], - key="pm_single_og" - ) - - codes = st.multiselect( - "Выберите коды строк", - [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], - default=[78, 79], - key="pm_single_codes" - ) - - columns = st.multiselect( - "Выберите столбцы", - ["БП", "ПП", "СЭБ", "Факт", "План"], - default=["БП", "ПП"], - key="pm_single_columns" - ) - - if st.button("🔍 Получить данные по ОГ", key="pm_single_btn"): - if codes and columns: - with st.spinner("Получаю данные..."): - data = { - "id": og_id, - "codes": codes, - "columns": columns - } - - result, status = make_api_request("/svodka_pm/get_single_og", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите коды и столбцы") - - with col2: - st.subheader("Данные по всем ОГ") - - codes_total = st.multiselect( - "Выберите коды строк", - [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], - default=[78, 79, 394, 395], - key="pm_total_codes" - ) - - columns_total = st.multiselect( - "Выберите столбцы", - ["БП", "ПП", "СЭБ", "Факт", "План"], - default=["БП", "ПП", "СЭБ"], - key="pm_total_columns" - ) - - if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"): - if codes_total and columns_total: - with st.spinner("Получаю данные..."): - data = { - "codes": codes_total, - "columns": columns_total - } - - result, status = make_api_request("/svodka_pm/get_total_ogs", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите коды и столбцы") - - # Вкладка 2: Сводки СА - полный функционал - with tab2: - st.header("🏭 Сводки СА - Полный функционал") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_ca = st.file_uploader( - "Выберите Excel файл сводки СА", - type=['xlsx', 'xlsm', 'xls'], - key="ca_upload" - ) - - if uploaded_ca is not None: - if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"): - with st.spinner("Загружаю файл..."): - try: - files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} - response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files) - result = response.json() - - if response.status_code == 200: - st.success(f"✅ {result.get('message', 'Файл загружен')}") - st.info(f"ID объекта: {result.get('object_id', 'N/A')}") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - except Exception as e: - st.error(f"❌ Ошибка: {str(e)}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Параметры запроса") - - modes = st.multiselect( - "Выберите режимы", - ["plan", "fact", "normativ"], - default=["plan", "fact"], - key="ca_modes" - ) - - tables = st.multiselect( - "Выберите таблицы", - ["ТиП", "Топливо", "Потери"], - default=["ТиП", "Топливо"], - key="ca_tables" - ) - - with col2: - st.subheader("Результат") - if st.button("🔍 Получить данные СА", key="ca_btn"): - if modes and tables: - with st.spinner("Получаю данные..."): - data = { - "modes": modes, - "tables": tables - } - - result, status = make_api_request("/svodka_ca/get_data", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите режимы и таблицы") - - # Вкладка 3: Мониторинг топлива - полный функционал - with tab3: - st.header("⛽ Мониторинг топлива - Полный функционал") - - # Секция загрузки файлов - st.subheader("📤 Загрузка файлов") - uploaded_fuel = st.file_uploader( - "Выберите ZIP архив с мониторингом топлива", - type=['zip'], - key="fuel_upload" - ) - - if uploaded_fuel is not None: - if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"): - with st.spinner("Загружаю файл..."): - result, status = upload_file_to_api( - "/monitoring_fuel/upload-zip", - uploaded_fuel.read(), - uploaded_fuel.name - ) - - if status == 200: - st.success(f"✅ {result.get('message', 'Файл загружен')}") - st.info(f"ID объекта: {result.get('object_id', 'N/A')}") - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - - st.markdown("---") - - # Секция получения данных - st.subheader("🔍 Получение данных") - - col1, col2 = st.columns(2) - - with col1: - st.subheader("Агрегация по колонкам") - - columns_fuel = st.multiselect( - "Выберите столбцы", - ["normativ", "total", "total_1"], - default=["normativ", "total"], - key="fuel_columns" - ) - - if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"): - if columns_fuel: - with st.spinner("Получаю данные..."): - data = { - "columns": columns_fuel - } - - result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - else: - st.warning("⚠️ Выберите столбцы") - - with col2: - st.subheader("Данные за месяц") - - month = st.selectbox( - "Выберите месяц", - [f"{i:02d}" for i in range(1, 13)], - key="fuel_month" - ) - - if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"): - with st.spinner("Получаю данные..."): - data = { - "month": month - } - - result, status = make_api_request("/monitoring_fuel/get_month_by_code", data) - - if status == 200: - st.success("✅ Данные получены") - st.json(result) - else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") - - # Футер - st.markdown("---") - st.markdown("### 📚 Документация API") - st.markdown(f"Полная документация доступна по адресу: {API_BASE_URL}/docs") - - # Информация о проекте - with st.expander("ℹ️ О проекте"): - st.markdown(""" - **NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов. - - **Возможности:** - - 📊 Парсинг сводок ПМ (план и факт) - - 🏭 Парсинг сводок СА - - ⛽ Мониторинг топлива - - **Технологии:** - - FastAPI - - Pandas - - MinIO (S3-совместимое хранилище) - - Streamlit (веб-интерфейс) - """) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/python_parser/run_streamlit_local.py b/run_streamlit_local.py similarity index 85% rename from python_parser/run_streamlit_local.py rename to run_streamlit_local.py index beef3e8..55fc202 100644 --- a/python_parser/run_streamlit_local.py +++ b/run_streamlit_local.py @@ -35,6 +35,7 @@ def main(): print("\n🚀 Запускаю Streamlit...") print("📍 URL: http://localhost:8501") + print("🔗 API: http://localhost:8000") print("🛑 Для остановки нажмите Ctrl+C") # Открываем браузер @@ -44,7 +45,11 @@ def main(): except Exception as e: print(f"⚠️ Не удалось открыть браузер: {e}") - # Запускаем Streamlit + # Запускаем Streamlit с правильными переменными окружения + env = os.environ.copy() + env["DOCKER_ENV"] = "false" # Локальный запуск + env["API_BASE_URL"] = "http://localhost:8000" # Локальный API + try: subprocess.run([ sys.executable, "-m", "streamlit", "run", "app.py", @@ -52,7 +57,7 @@ def main(): "--server.address", "localhost", "--server.headless", "false", "--browser.gatherUsageStats", "false" - ]) + ], env=env) except KeyboardInterrupt: print("\n👋 Streamlit остановлен") diff --git a/python_parser/streamlit_app/.dockerignore b/streamlit_app/.dockerignore similarity index 100% rename from python_parser/streamlit_app/.dockerignore rename to streamlit_app/.dockerignore diff --git a/python_parser/streamlit_app/Dockerfile b/streamlit_app/Dockerfile similarity index 100% rename from python_parser/streamlit_app/Dockerfile rename to streamlit_app/Dockerfile diff --git a/python_parser/streamlit_app/README.md b/streamlit_app/README.md similarity index 100% rename from python_parser/streamlit_app/README.md rename to streamlit_app/README.md diff --git a/python_parser/streamlit_app/app.py b/streamlit_app/app.py similarity index 95% rename from python_parser/streamlit_app/app.py rename to streamlit_app/app.py index 12cc93d..edf1209 100644 --- a/python_parser/streamlit_app/app.py +++ b/streamlit_app/app.py @@ -15,8 +15,17 @@ st.set_page_config( initial_sidebar_state="expanded" ) -# Конфигурация API - используем переменную окружения или значение по умолчанию -API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") +# Конфигурация API - автоматически определяем правильный адрес +def get_api_base_url(): + """Автоматически определяет правильный адрес API""" + # Если запущено в Docker, используем внутренний адрес + if os.getenv("DOCKER_ENV") == "true": + return "http://fastapi:8000" + + # Если запущено локально, используем localhost + return "http://localhost:8000" + +API_BASE_URL = os.getenv("API_BASE_URL", get_api_base_url()) def check_api_health(): """Проверка доступности API""" @@ -272,7 +281,7 @@ def main(): st.markdown("---") # Секция получения данных - st.subheader("🔍 Получение данных") + st.subheader("�� Получение данных") # Показываем доступные геттеры if getters_info and "getters" in getters_info: @@ -290,8 +299,8 @@ def main(): modes = st.multiselect( "Выберите режимы", - ["План", "Факт", "Норматив"], - default=["План", "Факт"], + ["plan", "fact", "normativ"], + default=["plan", "fact"], key="ca_modes" ) @@ -319,7 +328,7 @@ def main(): st.success("✅ Данные получены") st.json(result) else: - st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}") + st.error(f"❌ Ошибка: {result.get('message', f'Неизвестная ошибка: {status}')}") else: st.warning("⚠️ Выберите режимы и таблицы") diff --git a/python_parser/streamlit_app/requirements.txt b/streamlit_app/requirements.txt similarity index 100% rename from python_parser/streamlit_app/requirements.txt rename to streamlit_app/requirements.txt diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..f14d300 --- /dev/null +++ b/test_api.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +""" +Тестовый скрипт для проверки API +""" + +import requests +import json + +def test_api_endpoints(): + """Тестирование API эндпоинтов""" + base_url = "http://localhost:8000" + + print("🧪 ТЕСТИРОВАНИЕ API") + print("=" * 50) + + # Тест 1: Проверка доступности API + print("\n1️⃣ Проверка доступности API...") + try: + response = requests.get(f"{base_url}/") + if response.status_code == 200: + print(f"✅ API доступен: {response.json()}") + else: + print(f"❌ API недоступен: {response.status_code}") + return False + except Exception as e: + print(f"❌ Ошибка подключения к API: {e}") + return False + + # Тест 2: Список парсеров + print("\n2️⃣ Получение списка парсеров...") + try: + response = requests.get(f"{base_url}/parsers") + if response.status_code == 200: + parsers = response.json() + print(f"✅ Парсеры: {parsers}") + else: + print(f"❌ Ошибка получения парсеров: {response.status_code}") + except Exception as e: + print(f"❌ Ошибка: {e}") + + # Тест 3: Информация о геттерах + print("\n3️⃣ Информация о геттерах парсеров...") + parsers_to_test = ["svodka_pm", "svodka_ca", "monitoring_fuel"] + + for parser in parsers_to_test: + try: + response = requests.get(f"{base_url}/parsers/{parser}/getters") + if response.status_code == 200: + getters = response.json() + print(f"✅ {parser}: {len(getters.get('getters', {}))} геттеров") + else: + print(f"❌ {parser}: ошибка {response.status_code}") + except Exception as e: + print(f"❌ {parser}: ошибка {e}") + + # Тест 4: Загрузка тестового файла + print("\n4️⃣ Тест загрузки файла...") + try: + # Создаем простой Excel файл для теста + test_data = b"test content" + files = {"file": ("test.xlsx", test_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + + response = requests.post(f"{base_url}/svodka_ca/upload", files=files) + print(f"📤 Результат загрузки: {response.status_code}") + + if response.status_code == 200: + result = response.json() + print(f"✅ Файл загружен: {result}") + else: + print(f"❌ Ошибка загрузки: {response.status_code}") + try: + error_detail = response.json() + print(f"📋 Детали ошибки: {error_detail}") + except: + print(f"📋 Текст ошибки: {response.text}") + + except Exception as e: + print(f"❌ Ошибка теста загрузки: {e}") + + print("\n🎯 Тестирование завершено!") + return True + +if __name__ == "__main__": + test_api_endpoints() \ No newline at end of file diff --git a/test_api_direct.py b/test_api_direct.py new file mode 100644 index 0000000..e58aac6 --- /dev/null +++ b/test_api_direct.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +""" +Прямое тестирование API эндпоинтов +""" + +import requests +import json + +def test_api_endpoints(): + """Тестирование API эндпоинтов""" + base_url = "http://localhost:8000" + + print("🧪 ПРЯМОЕ ТЕСТИРОВАНИЕ API") + print("=" * 40) + + # Тест 1: Проверка доступности API + print("\n1️⃣ Проверка доступности API...") + try: + response = requests.get(f"{base_url}/") + print(f"✅ API доступен: {response.status_code}") + except Exception as e: + print(f"❌ Ошибка: {e}") + return + + # Тест 2: Тестирование эндпоинта svodka_ca/get_data + print("\n2️⃣ Тестирование svodka_ca/get_data...") + try: + data = { + "getter": "get_data", + "modes": ["plan", "fact"], + "tables": ["ТиП", "Топливо"] + } + + response = requests.post(f"{base_url}/svodka_ca/get_data", json=data) + print(f"📥 Результат: {response.status_code}") + + if response.status_code == 200: + result = response.json() + print(f"✅ Успешно: {result}") + else: + try: + error_detail = response.json() + print(f"❌ Ошибка: {error_detail}") + except: + print(f"❌ Ошибка: {response.text}") + + except Exception as e: + print(f"❌ Исключение: {e}") + + # Тест 3: Тестирование эндпоинта svodka_pm/get_data + print("\n3️⃣ Тестирование svodka_pm/get_data...") + try: + data = { + "getter": "single_og", + "id": "SNPZ", + "codes": [78, 79], + "columns": ["БП", "ПП"] + } + + response = requests.post(f"{base_url}/svodka_pm/get_data", json=data) + print(f"📥 Результат: {response.status_code}") + + if response.status_code == 200: + result = response.json() + print(f"✅ Успешно: {result}") + else: + try: + error_detail = response.json() + print(f"❌ Ошибка: {error_detail}") + except: + print(f"❌ Ошибка: {response.text}") + + except Exception as e: + print(f"❌ Исключение: {e}") + + print("\n🎯 Тестирование завершено!") + +if __name__ == "__main__": + test_api_endpoints() \ No newline at end of file diff --git a/test_ca_workflow.py b/test_ca_workflow.py new file mode 100644 index 0000000..61fe68e --- /dev/null +++ b/test_ca_workflow.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Тестирование полного workflow с сводкой СА +""" + +import requests +import os +import time + +def test_ca_workflow(): + """Тестирование полного workflow с сводкой СА""" + base_url = "http://localhost:8000" + test_file = "python_parser/data/svodka_ca.xlsx" + + print("🧪 ТЕСТ ПОЛНОГО WORKFLOW СВОДКИ СА") + print("=" * 50) + + # Проверяем, что файл существует + if not os.path.exists(test_file): + print(f"❌ Файл {test_file} не найден") + return False + + print(f"📁 Тестовый файл найден: {test_file}") + print(f"📏 Размер: {os.path.getsize(test_file)} байт") + + # Шаг 1: Загружаем файл + print("\n1️⃣ Загружаю файл сводки СА...") + try: + with open(test_file, 'rb') as f: + file_data = f.read() + + files = {"file": ("svodka_ca.xlsx", file_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + + response = requests.post(f"{base_url}/svodka_ca/upload", files=files) + print(f"📤 Результат загрузки: {response.status_code}") + + if response.status_code == 200: + result = response.json() + print(f"✅ Файл загружен: {result}") + object_id = result.get('object_id', 'nin_excel_data_svodka_ca') + else: + print(f"❌ Ошибка загрузки: {response.status_code}") + try: + error_detail = response.json() + print(f"📋 Детали ошибки: {error_detail}") + except: + print(f"📋 Текст ошибки: {response.text}") + return False + + except Exception as e: + print(f"❌ Ошибка загрузки: {e}") + return False + + # Шаг 2: Получаем данные через геттер + print("\n2️⃣ Получаю данные через геттер...") + try: + data = { + "getter": "get_data", + "modes": ["plan", "fact"], # Используем английские названия + "tables": ["ТиП", "Топливо"] + } + + response = requests.post(f"{base_url}/svodka_ca/get_data", json=data) + print(f"📥 Результат получения данных: {response.status_code}") + + if response.status_code == 200: + result = response.json() + print(f"✅ Данные получены успешно!") + print(f"📊 Размер ответа: {len(str(result))} символов") + + # Показываем структуру данных + if isinstance(result, dict): + print(f"🔍 Структура данных:") + for key, value in result.items(): + if isinstance(value, dict): + print(f" {key}: {len(value)} элементов") + else: + print(f" {key}: {type(value).__name__}") + else: + print(f"❌ Ошибка получения данных: {response.status_code}") + try: + error_detail = response.json() + print(f"📋 Детали ошибки: {error_detail}") + except: + print(f"📋 Текст ошибки: {response.text}") + return False + + except Exception as e: + print(f"❌ Ошибка получения данных: {e}") + return False + + print("\n🎯 Тестирование завершено успешно!") + return True + +if __name__ == "__main__": + test_ca_workflow() \ No newline at end of file diff --git a/test_minio_connection.py b/test_minio_connection.py new file mode 100644 index 0000000..174ec85 --- /dev/null +++ b/test_minio_connection.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Тестовый скрипт для проверки подключения к MinIO +""" + +import os +import sys +import io +from minio import Minio + +def test_minio_connection(): + """Тестирование подключения к MinIO""" + print("🔍 Тестирование подключения к MinIO...") + + # Параметры подключения + endpoint = os.getenv("MINIO_ENDPOINT", "localhost:9000") + access_key = os.getenv("MINIO_ACCESS_KEY", "minioadmin") + secret_key = os.getenv("MINIO_SECRET_KEY", "minioadmin") + bucket_name = os.getenv("MINIO_BUCKET", "svodka-data") + + print(f"📍 Endpoint: {endpoint}") + print(f"🔑 Access Key: {access_key}") + print(f"🔐 Secret Key: {secret_key}") + print(f"🪣 Bucket: {bucket_name}") + + try: + # Создаем клиент + print("\n🚀 Создаю MinIO клиент...") + client = Minio( + endpoint, + access_key=access_key, + secret_key=secret_key, + secure=False, + cert_check=False + ) + + # Проверяем подключение + print("✅ MinIO клиент создан") + + # Проверяем bucket + print(f"\n🔍 Проверяю bucket '{bucket_name}'...") + if client.bucket_exists(bucket_name): + print(f"✅ Bucket '{bucket_name}' существует") + else: + print(f"⚠️ Bucket '{bucket_name}' не существует, создаю...") + client.make_bucket(bucket_name) + print(f"✅ Bucket '{bucket_name}' создан") + + # Пробуем загрузить тестовый файл + print("\n📤 Тестирую загрузку файла...") + test_data = b"Hello MinIO!" + test_stream = io.BytesIO(test_data) + + client.put_object( + bucket_name, + "test.txt", + test_stream, + length=len(test_data), + content_type='text/plain' + ) + print("✅ Тестовый файл загружен") + + # Пробуем скачать файл + print("\n📥 Тестирую скачивание файла...") + response = client.get_object(bucket_name, "test.txt") + downloaded_data = response.read() + print(f"✅ Файл скачан: {downloaded_data}") + + # Удаляем тестовый файл + client.remove_object(bucket_name, "test.txt") + print("✅ Тестовый файл удален") + + print("\n🎉 Все тесты MinIO прошли успешно!") + return True + + except Exception as e: + print(f"\n❌ Ошибка подключения к MinIO: {e}") + print(f"Тип ошибки: {type(e).__name__}") + return False + +def test_environment(): + """Проверка переменных окружения""" + print("🔧 Проверка переменных окружения:") + env_vars = [ + "MINIO_ENDPOINT", + "MINIO_ACCESS_KEY", + "MINIO_SECRET_KEY", + "MINIO_BUCKET" + ] + + for var in env_vars: + value = os.getenv(var, "НЕ УСТАНОВЛЕНО") + print(f" {var}: {value}") + +if __name__ == "__main__": + print("=" * 60) + print("🧪 ТЕСТ ПОДКЛЮЧЕНИЯ К MINIO") + print("=" * 60) + + test_environment() + print() + + success = test_minio_connection() + + if success: + print("\n✅ MinIO работает корректно!") + sys.exit(0) + else: + print("\n❌ Проблемы с MinIO!") + sys.exit(1) \ No newline at end of file diff --git a/test_upload.py b/test_upload.py new file mode 100644 index 0000000..6559dec --- /dev/null +++ b/test_upload.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +Тестирование загрузки Excel файла +""" + +import requests +import os + +def test_file_upload(): + """Тестирование загрузки файла""" + base_url = "http://localhost:8000" + filename = "test_file.xlsx" + + print("🧪 ТЕСТ ЗАГРУЗКИ ФАЙЛА") + print("=" * 40) + + # Проверяем, что файл существует + if not os.path.exists(filename): + print(f"❌ Файл {filename} не найден") + return False + + print(f"📁 Файл найден: {filename}") + print(f"📏 Размер: {os.path.getsize(filename)} байт") + + # Тестируем загрузку в разные парсеры + parsers = [ + ("svodka_ca", "/svodka_ca/upload", "file"), + ("monitoring_fuel", "/monitoring_fuel/upload-zip", "zip_file"), + ("svodka_pm", "/svodka_pm/upload-zip", "zip_file") + ] + + for parser_name, endpoint, file_param in parsers: + print(f"\n🔍 Тестирую {parser_name}...") + + try: + # Читаем файл + with open(filename, 'rb') as f: + file_data = f.read() + + # Определяем content type + if filename.endswith('.xlsx'): + content_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + else: + content_type = "application/octet-stream" + + # Загружаем файл с правильным параметром + files = {file_param: (filename, file_data, content_type)} + + response = requests.post(f"{base_url}{endpoint}", files=files) + print(f"📤 Результат: {response.status_code}") + + if response.status_code == 200: + result = response.json() + print(f"✅ Успешно: {result}") + else: + try: + error_detail = response.json() + print(f"❌ Ошибка: {error_detail}") + except: + print(f"❌ Ошибка: {response.text}") + + except Exception as e: + print(f"❌ Исключение: {e}") + + print("\n🎯 Тестирование завершено!") + return True + +if __name__ == "__main__": + test_file_upload() \ No newline at end of file