Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72fe115a99 | |||
| 46a30c32ed | |||
| 5e217c7cce | |||
| 7d2747c8fe | |||
| 513ff3c144 | |||
| a0b6e04d99 | |||
| 47a7344755 | |||
| 456e9935f0 |
204
.gitignore
vendored
204
.gitignore
vendored
@@ -1,15 +1,8 @@
|
|||||||
data
|
# Python
|
||||||
.streamlit
|
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
@@ -23,88 +16,13 @@ parts/
|
|||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
pip-wheel-metadata/
|
|
||||||
share/python-wheels/
|
share/python-wheels/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
|
||||||
# PyInstaller
|
# Virtual environments
|
||||||
# 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
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
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
|
.env
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
@@ -113,6 +31,86 @@ ENV/
|
|||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
log/
|
||||||
|
|
||||||
|
# MinIO data and cache
|
||||||
|
minio_data/
|
||||||
|
.minio.sys/
|
||||||
|
*.meta
|
||||||
|
part.*
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*.orig
|
||||||
|
|
||||||
|
# Data files (Excel, CSV, etc.)
|
||||||
|
*.xlsx
|
||||||
|
*.xls
|
||||||
|
*.xlsm
|
||||||
|
*.csv
|
||||||
|
*.json
|
||||||
|
data/
|
||||||
|
uploads/
|
||||||
|
|
||||||
|
# Cache directories
|
||||||
|
.cache/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# Celery
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
.spyproject
|
.spyproject
|
||||||
@@ -131,23 +129,27 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
# IDE
|
# pytype static type analyzer
|
||||||
.vscode/
|
.pytype/
|
||||||
.idea/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# OS
|
# Cython debug symbols
|
||||||
.DS_Store
|
cython_debug/
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Project specific
|
# Local development
|
||||||
data/
|
local_settings.py
|
||||||
*.zip
|
db.sqlite3
|
||||||
*.xlsx
|
db.sqlite3-journal
|
||||||
*.xls
|
|
||||||
*.xlsm
|
|
||||||
|
|
||||||
# MinIO data directory
|
# FastAPI
|
||||||
minio_data/
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Streamlit
|
||||||
|
.streamlit/secrets.toml
|
||||||
|
|
||||||
|
# Node.js (if any frontend components)
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
41
QUICK_START.md
Normal file
41
QUICK_START.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 🚀 Быстрый запуск проекта
|
||||||
|
|
||||||
|
## 1. Запуск всех сервисов
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Проверка статуса
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Доступ к сервисам
|
||||||
|
- **FastAPI**: http://localhost:8000
|
||||||
|
- **Streamlit**: http://localhost:8501
|
||||||
|
- **MinIO Console**: http://localhost:9001
|
||||||
|
- **MinIO API**: http://localhost:9000
|
||||||
|
|
||||||
|
## 4. Остановка
|
||||||
|
```bash
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Просмотр логов
|
||||||
|
```bash
|
||||||
|
# Все сервисы
|
||||||
|
docker compose logs
|
||||||
|
|
||||||
|
# Конкретный сервис
|
||||||
|
docker compose logs fastapi
|
||||||
|
docker compose logs streamlit
|
||||||
|
docker compose logs minio
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Пересборка и перезапуск
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
**Примечание**: При первом запуске Docker будет скачивать образы и собирать контейнеры, это может занять несколько минут.
|
||||||
227
README.md
227
README.md
@@ -1,182 +1,117 @@
|
|||||||
# 🚀 NIN Excel Parsers API - Полная система
|
# Python Parser CF - Система анализа данных
|
||||||
|
|
||||||
Полноценная система для парсинга Excel отчетов нефтеперерабатывающих заводов (НПЗ) с использованием FastAPI, MinIO и Streamlit.
|
Проект состоит из трех основных компонентов:
|
||||||
|
- **python_parser** - FastAPI приложение для парсинга и обработки данных
|
||||||
## 🏗️ Архитектура проекта
|
- **streamlit_app** - Streamlit приложение для визуализации и анализа
|
||||||
|
- **minio_data** - хранилище данных MinIO
|
||||||
Проект состоит из **двух изолированных пакетов**:
|
|
||||||
|
|
||||||
- **`python_parser/`** - FastAPI сервер + парсеры Excel
|
|
||||||
- **`streamlit_app/`** - Веб-интерфейс для демонстрации API
|
|
||||||
|
|
||||||
## 🚀 Быстрый запуск
|
## 🚀 Быстрый запуск
|
||||||
|
|
||||||
### **Вариант 1: Все сервисы в Docker (рекомендуется)**
|
### Предварительные требования
|
||||||
```bash
|
- Docker и Docker Compose
|
||||||
# Запуск всех сервисов: MinIO + FastAPI + Streamlit
|
- Git
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Доступ:
|
### Запуск всех сервисов (продакшн)
|
||||||
# - MinIO Console: http://localhost:9001
|
```bash
|
||||||
# - FastAPI: http://localhost:8000
|
docker compose up -d
|
||||||
# - Streamlit: http://localhost:8501
|
|
||||||
# - API Docs: http://localhost:8000/docs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Вариант 2: Только MinIO в Docker + сервисы локально**
|
### Запуск в режиме разработки
|
||||||
```bash
|
```bash
|
||||||
# Запуск MinIO в Docker
|
# Автоматический запуск
|
||||||
docker-compose up -d minio
|
python start_dev.py
|
||||||
|
|
||||||
# Запуск FastAPI локально
|
# Или вручную
|
||||||
cd python_parser
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
python run_dev.py
|
|
||||||
|
|
||||||
# В отдельном терминале - Streamlit
|
|
||||||
cd streamlit_app
|
|
||||||
streamlit run app.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Вариант 3: Только MinIO в Docker**
|
**Режим разработки** позволяет:
|
||||||
|
- Автоматически перезагружать Streamlit при изменении кода
|
||||||
|
- Монтировать исходный код напрямую в контейнер
|
||||||
|
- Видеть изменения без пересборки контейнеров
|
||||||
|
|
||||||
|
### Доступ к сервисам
|
||||||
|
- **FastAPI**: http://localhost:8000
|
||||||
|
- **Streamlit**: http://localhost:8501
|
||||||
|
- **MinIO Console**: http://localhost:9001
|
||||||
|
- **MinIO API**: http://localhost:9000
|
||||||
|
|
||||||
|
### Остановка сервисов
|
||||||
```bash
|
```bash
|
||||||
# Запуск только MinIO
|
docker-compose down
|
||||||
docker-compose up -d minio
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📋 Описание сервисов
|
|
||||||
|
|
||||||
- **MinIO** (порт 9000-9001): S3-совместимое хранилище для данных
|
|
||||||
- **FastAPI** (порт 8000): API сервер для парсинга Excel файлов
|
|
||||||
- **Streamlit** (порт 8501): Веб-интерфейс для демонстрации API
|
|
||||||
|
|
||||||
## 📁 Структура проекта
|
## 📁 Структура проекта
|
||||||
|
|
||||||
```
|
```
|
||||||
python_parser_cf/ # Корень проекта
|
python_parser_cf/
|
||||||
├── python_parser/ # Пакет FastAPI + парсеры
|
├── python_parser/ # FastAPI приложение
|
||||||
│ ├── app/ # FastAPI приложение
|
│ ├── app/ # Основной код приложения
|
||||||
│ │ ├── main.py # Основной файл приложения
|
│ ├── adapters/ # Адаптеры для парсеров
|
||||||
│ │ └── schemas/ # Pydantic схемы
|
│ ├── core/ # Основная бизнес-логика
|
||||||
│ ├── core/ # Бизнес-логика
|
│ ├── data/ # Тестовые данные
|
||||||
│ │ ├── models.py # Модели данных
|
│ └── Dockerfile # Docker образ для FastAPI
|
||||||
│ │ ├── ports.py # Интерфейсы (порты)
|
├── streamlit_app/ # Streamlit приложение
|
||||||
│ │ └── services.py # Сервисы
|
│ ├── streamlit_app.py # Основной файл приложения
|
||||||
│ ├── adapters/ # Адаптеры для внешних систем
|
│ ├── requirements.txt # Зависимости Python
|
||||||
│ │ ├── storage.py # MinIO адаптер
|
│ ├── .streamlit/ # Конфигурация Streamlit
|
||||||
│ │ └── parsers/ # Парсеры Excel файлов
|
│ └── Dockerfile # Docker образ для Streamlit
|
||||||
│ ├── data/ # Тестовые данные
|
├── minio_data/ # Данные для MinIO
|
||||||
│ ├── Dockerfile # Docker образ для FastAPI
|
├── docker-compose.yml # Конфигурация всех сервисов
|
||||||
│ ├── requirements.txt # Зависимости FastAPI
|
└── README.md # Документация
|
||||||
│ └── 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** - Список доступных парсеров
|
- `MINIO_ENDPOINT` - адрес MinIO сервера
|
||||||
- **GET /parsers/{parser_name}/getters** - Информация о геттерах парсера
|
- `MINIO_ACCESS_KEY` - ключ доступа к MinIO
|
||||||
- **POST /svodka_pm/upload-zip** - Загрузка сводок ПМ
|
- `MINIO_SECRET_KEY` - секретный ключ MinIO
|
||||||
- **POST /svodka_ca/upload** - Загрузка сводок ЦА
|
- `MINIO_SECURE` - использование SSL/TLS
|
||||||
- **POST /monitoring_fuel/upload-zip** - Загрузка мониторинга топлива
|
- `MINIO_BUCKET` - имя bucket'а для данных
|
||||||
- **POST /svodka_pm/get_data** - Получение данных сводок ПМ
|
|
||||||
- **POST /svodka_ca/get_data** - Получение данных сводок ЦА
|
|
||||||
- **POST /monitoring_fuel/get_data** - Получение данных мониторинга топлива
|
|
||||||
|
|
||||||
## 📊 Поддерживаемые типы отчетов
|
### Порты
|
||||||
|
- **8000** - FastAPI
|
||||||
|
- **8501** - Streamlit
|
||||||
|
- **9000** - MinIO API
|
||||||
|
- **9001** - MinIO Console
|
||||||
|
|
||||||
1. **svodka_pm** - Сводки по переработке нефти (ПМ)
|
## 📊 Использование
|
||||||
- Геттеры: `single_og`, `total_ogs`
|
|
||||||
2. **svodka_ca** - Сводки по переработке нефти (ЦА)
|
|
||||||
- Геттеры: `get_data`
|
|
||||||
3. **monitoring_fuel** - Мониторинг топлива
|
|
||||||
- Геттеры: `total_by_columns`, `month_by_code`
|
|
||||||
|
|
||||||
## 🏗️ Архитектура
|
1. **Запустите все сервисы**: `docker-compose up -d`
|
||||||
|
2. **Откройте Streamlit**: http://localhost:8501
|
||||||
|
3. **Выберите тип данных** для анализа
|
||||||
|
4. **Просматривайте результаты** в интерактивном интерфейсе
|
||||||
|
|
||||||
Проект использует **Hexagonal Architecture (Ports and Adapters)**:
|
## 🛠️ Разработка
|
||||||
|
|
||||||
- **Порты (Ports)**: Интерфейсы для бизнес-логики
|
### Режим разработки (рекомендуется)
|
||||||
- **Адаптеры (Adapters)**: Реализации для внешних систем
|
|
||||||
- **Сервисы (Services)**: Бизнес-логика приложения
|
|
||||||
|
|
||||||
### Система геттеров парсеров
|
|
||||||
|
|
||||||
Каждый парсер может иметь несколько методов получения данных (геттеров):
|
|
||||||
- Регистрация геттеров в словаре с метаданными
|
|
||||||
- Валидация параметров для каждого геттера
|
|
||||||
- Единый интерфейс `get_value(getter_name, params)`
|
|
||||||
|
|
||||||
## 🐳 Docker
|
|
||||||
|
|
||||||
### Сборка образов:
|
|
||||||
```bash
|
```bash
|
||||||
# FastAPI
|
# Запуск режима разработки
|
||||||
docker build -t nin-fastapi ./python_parser
|
python start_dev.py
|
||||||
|
|
||||||
# Streamlit
|
# Остановка
|
||||||
docker build -t nin-streamlit ./streamlit_app
|
docker compose -f docker-compose.dev.yml down
|
||||||
|
|
||||||
|
# Возврат к продакшн режиму
|
||||||
|
python start_prod.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Запуск отдельных сервисов:
|
### Локальная разработка FastAPI
|
||||||
```bash
|
```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
|
cd python_parser
|
||||||
pytest
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
# Запуск с покрытием
|
### Локальная разработка Streamlit
|
||||||
pytest --cov=.
|
```bash
|
||||||
|
cd streamlit_app
|
||||||
|
pip install -r requirements.txt
|
||||||
|
streamlit run streamlit_app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📝 Лицензия
|
## 📝 Лицензия
|
||||||
|
|
||||||
Проект разработан для внутреннего использования НИН.
|
Проект разработан для внутреннего использования.
|
||||||
@@ -170,11 +170,16 @@ def main():
|
|||||||
|
|
||||||
if not port_8000_ok:
|
if not port_8000_ok:
|
||||||
print("\n🔧 РЕШЕНИЕ: Запустите FastAPI сервер")
|
print("\n🔧 РЕШЕНИЕ: Запустите FastAPI сервер")
|
||||||
print("docker-compose up -d fastapi")
|
print("python run_dev.py")
|
||||||
|
|
||||||
if not port_8501_ok:
|
if not port_8501_ok:
|
||||||
print("\n🔧 РЕШЕНИЕ: Запустите Streamlit")
|
print("\n🔧 РЕШЕНИЕ: Запустите Streamlit")
|
||||||
print("docker-compose up -d streamlit")
|
print("python run_streamlit.py")
|
||||||
|
|
||||||
|
print("\n🚀 Для автоматического запуска используйте:")
|
||||||
|
print("python start_demo.py")
|
||||||
|
print("\n🔍 Для пошагового запуска используйте:")
|
||||||
|
print("python run_manual.py")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
58
docker-compose.dev.yml
Normal file
58
docker-compose.dev.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
services:
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
container_name: svodka_minio_dev
|
||||||
|
ports:
|
||||||
|
- "9000:9000" # API порт
|
||||||
|
- "9001:9001" # Консоль порт
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD: minioadmin
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
volumes:
|
||||||
|
- ./minio_data:/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
fastapi:
|
||||||
|
build: ./python_parser
|
||||||
|
container_name: svodka_fastapi_dev
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
- MINIO_ENDPOINT=minio:9000
|
||||||
|
- MINIO_ACCESS_KEY=minioadmin
|
||||||
|
- MINIO_SECRET_KEY=minioadmin
|
||||||
|
- MINIO_SECURE=false
|
||||||
|
- MINIO_BUCKET=svodka-data
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
streamlit:
|
||||||
|
image: python:3.11-slim
|
||||||
|
container_name: svodka_streamlit_dev
|
||||||
|
ports:
|
||||||
|
- "8501:8501"
|
||||||
|
environment:
|
||||||
|
- API_BASE_URL=http://fastapi:8000
|
||||||
|
- API_PUBLIC_URL=http://localhost:8000
|
||||||
|
- MINIO_ENDPOINT=minio:9000
|
||||||
|
- MINIO_ACCESS_KEY=minioadmin
|
||||||
|
- MINIO_SECRET_KEY=minioadmin
|
||||||
|
- MINIO_SECURE=false
|
||||||
|
- MINIO_BUCKET=svodka-data
|
||||||
|
volumes:
|
||||||
|
# Монтируем исходный код для автоматической перезагрузки
|
||||||
|
- ./streamlit_app:/app
|
||||||
|
# Монтируем requirements.txt для установки зависимостей
|
||||||
|
- ./streamlit_app/requirements.txt:/app/requirements.txt
|
||||||
|
working_dir: /app
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
- fastapi
|
||||||
|
restart: unless-stopped
|
||||||
|
command: >
|
||||||
|
bash -c "
|
||||||
|
pip install --no-cache-dir -r requirements.txt &&
|
||||||
|
streamlit run streamlit_app.py --server.port=8501 --server.address=0.0.0.0 --server.runOnSave=true
|
||||||
|
"
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Продакшн конфигурация
|
||||||
|
# Для разработки используйте: docker compose -f docker-compose.dev.yml up -d
|
||||||
services:
|
services:
|
||||||
minio:
|
minio:
|
||||||
image: minio/minio:latest
|
image: minio/minio:latest
|
||||||
@@ -35,7 +37,13 @@ services:
|
|||||||
- "8501:8501"
|
- "8501:8501"
|
||||||
environment:
|
environment:
|
||||||
- API_BASE_URL=http://fastapi:8000
|
- API_BASE_URL=http://fastapi:8000
|
||||||
- DOCKER_ENV=true
|
- API_PUBLIC_URL=http://localhost:8000
|
||||||
|
- MINIO_ENDPOINT=minio:9000
|
||||||
|
- MINIO_ACCESS_KEY=minioadmin
|
||||||
|
- MINIO_SECRET_KEY=minioadmin
|
||||||
|
- MINIO_SECURE=false
|
||||||
|
- MINIO_BUCKET=svodka-data
|
||||||
depends_on:
|
depends_on:
|
||||||
|
- minio
|
||||||
- fastapi
|
- fastapi
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
17
manifest.yml
17
manifest.yml
@@ -1,17 +0,0 @@
|
|||||||
applications:
|
|
||||||
- name: nin-python-parser-dev-test
|
|
||||||
buildpack: python_buildpack
|
|
||||||
health-check-type: web
|
|
||||||
services:
|
|
||||||
- logging-shared-dev
|
|
||||||
command: python /app/run_stand.py
|
|
||||||
path: .
|
|
||||||
disk_quota: 2G
|
|
||||||
memory: 4G
|
|
||||||
instances: 1
|
|
||||||
env:
|
|
||||||
MINIO_ENDPOINT: s3-region1.ppc-jv-dev.sibintek.ru
|
|
||||||
MINIO_ACCESS_KEY: 00a70fac02c1208446de
|
|
||||||
MINIO_SECRET_KEY: 1gk9tVYEEoH9ADRxb4kiAuCo6CCISdV6ie0p6oDO
|
|
||||||
MINIO_BUCKET: bucket-476684e7-1223-45ac-a101-8b5aeda487d6
|
|
||||||
MINIO_SECURE: false
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
FROM repo-dev.predix.rosneft.ru/python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# RUN pip install kafka-python==2.0.2
|
|
||||||
# RUN pip freeze > /app/requirements.txt
|
|
||||||
|
|
||||||
# ADD . /app
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
RUN mkdir -p vendor
|
|
||||||
RUN pip download -r /app/requirements.txt --no-binary=:none: -d /app/vendor
|
|
||||||
|
|
||||||
# ADD . /app
|
|
||||||
|
|
||||||
# ENV KAFKA_BROKER=10.234.160.10:9093,10.234.160.10:9094,10.234.160.10:9095
|
|
||||||
# ENV KAFKA_UPDATE_ALGORITHM_RULES_TOPIC=algorithm-rule-update
|
|
||||||
# ENV KAFKA_CLIENT_USERNAME=cf-service
|
|
||||||
|
|
||||||
# CMD ["python", "/app/run_dev.py"]
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
# 📊 Python Parser - FastAPI + Парсеры Excel
|
|
||||||
|
|
||||||
Пакет FastAPI сервера и парсеров Excel для нефтеперерабатывающих заводов.
|
|
||||||
|
|
||||||
## 🚀 Быстрый запуск
|
|
||||||
|
|
||||||
### **Локально:**
|
|
||||||
```bash
|
|
||||||
# Установка зависимостей
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Запуск FastAPI сервера
|
|
||||||
python run_dev.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### **В Docker:**
|
|
||||||
```bash
|
|
||||||
# Сборка образа
|
|
||||||
docker build -t nin-fastapi .
|
|
||||||
|
|
||||||
# Запуск контейнера
|
|
||||||
docker run -p 8000:8000 nin-fastapi
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Структура пакета
|
|
||||||
|
|
||||||
```
|
|
||||||
python_parser/
|
|
||||||
├── 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 # Зависимости Python
|
|
||||||
└── run_dev.py # Запуск FastAPI локально
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Основные эндпоинты
|
|
||||||
|
|
||||||
- **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)`
|
|
||||||
|
|
||||||
## 🔧 Разработка
|
|
||||||
|
|
||||||
### Добавление нового парсера:
|
|
||||||
|
|
||||||
1. Создайте файл в `adapters/parsers/`
|
|
||||||
2. Реализуйте интерфейс `ParserPort`
|
|
||||||
3. Добавьте в `core/services.py`
|
|
||||||
4. Создайте схемы в `app/schemas/`
|
|
||||||
5. Добавьте эндпоинты в `app/main.py`
|
|
||||||
|
|
||||||
### Тестирование:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Запуск тестов
|
|
||||||
pytest
|
|
||||||
|
|
||||||
# Запуск с покрытием
|
|
||||||
pytest --cov=.
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Примечание
|
|
||||||
|
|
||||||
Этот пакет является частью большей системы. Для полной документации и запуска всех сервисов см. README.md в корне проекта.
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import re
|
import re
|
||||||
import zipfile
|
from typing import Dict
|
||||||
from typing import Dict, Tuple
|
|
||||||
from core.ports import ParserPort
|
from core.ports import ParserPort
|
||||||
from adapters.pconfig import data_to_json
|
from adapters.pconfig import data_to_json, get_object_by_name
|
||||||
|
|
||||||
|
|
||||||
class MonitoringFuelParser(ParserPort):
|
class MonitoringFuelParser(ParserPort):
|
||||||
@@ -11,55 +11,71 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
|
|
||||||
name = "Мониторинг топлива"
|
name = "Мониторинг топлива"
|
||||||
|
|
||||||
def _register_default_getters(self):
|
def find_header_row(self, file_path: str, sheet: str, search_value: str = "Установка", max_rows: int = 50) -> int:
|
||||||
"""Регистрация геттеров по умолчанию"""
|
"""Определение индекса заголовка в Excel по ключевому слову"""
|
||||||
self.register_getter(
|
# Читаем первые max_rows строк без заголовков
|
||||||
name="total_by_columns",
|
df_temp = pd.read_excel(
|
||||||
method=self._get_total_by_columns,
|
file_path,
|
||||||
required_params=["columns"],
|
sheet_name=sheet,
|
||||||
optional_params=[],
|
header=None,
|
||||||
description="Агрегация данных по колонкам"
|
nrows=max_rows
|
||||||
)
|
|
||||||
|
|
||||||
self.register_getter(
|
|
||||||
name="month_by_code",
|
|
||||||
method=self._get_month_by_code,
|
|
||||||
required_params=["month"],
|
|
||||||
optional_params=[],
|
|
||||||
description="Получение данных за конкретный месяц"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_total_by_columns(self, params: dict):
|
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
||||||
"""Агрегация по колонкам (обертка для совместимости)"""
|
for idx, row in df_temp.iterrows():
|
||||||
columns = params["columns"]
|
if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
|
||||||
if not columns:
|
print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
|
||||||
raise ValueError("Отсутствуют идентификаторы столбцов")
|
return idx + 1 # возвращаем индекс строки (0-based)
|
||||||
|
|
||||||
# TODO: Переделать под новую архитектуру
|
|
||||||
df_means, _ = self.aggregate_by_columns(self.df, columns)
|
|
||||||
return df_means.to_dict(orient='index')
|
|
||||||
|
|
||||||
def _get_month_by_code(self, params: dict):
|
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
||||||
"""Получение данных за месяц (обертка для совместимости)"""
|
|
||||||
month = params["month"]
|
|
||||||
if not month:
|
|
||||||
raise ValueError("Отсутствует идентификатор месяца")
|
|
||||||
|
|
||||||
# TODO: Переделать под новую архитектуру
|
|
||||||
df_month = self.get_month(self.df, month)
|
|
||||||
return df_month.to_dict(orient='index')
|
|
||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
def parse_single(self, file, sheet, header_num=None):
|
||||||
"""Парсинг файла и возврат DataFrame"""
|
''' Собственно парсер отчетов одного объекта'''
|
||||||
# Сохраняем DataFrame для использования в геттерах
|
# Автоопределение header_num, если не передан
|
||||||
self.df = self.parse_monitoring_fuel_files(file_path, params)
|
if header_num is None:
|
||||||
return self.df
|
header_num = self.find_header_row(file, sheet, search_value="Установка")
|
||||||
|
# Читаем весь лист, начиная с найденной строки как заголовок
|
||||||
|
df_full = pd.read_excel(
|
||||||
|
file,
|
||||||
|
sheet_name=sheet,
|
||||||
|
header=header_num,
|
||||||
|
usecols=None,
|
||||||
|
index_col=None
|
||||||
|
)
|
||||||
|
|
||||||
def parse_monitoring_fuel_files(self, zip_path: str, params: dict) -> Dict[str, pd.DataFrame]:
|
# === Удаление полностью пустых столбцов ===
|
||||||
"""Парсинг ZIP архива с файлами мониторинга топлива"""
|
df_clean = df_full.replace(r'^\s*$', pd.NA, regex=True) # заменяем пустые строки на NA
|
||||||
|
df_clean = df_clean.dropna(axis=1, how='all') # удаляем столбцы, где все значения — NA
|
||||||
|
df_full = df_full.loc[:, df_clean.columns] # оставляем только непустые столбцы
|
||||||
|
|
||||||
|
# === Переименовываем нужные столбцы по позициям ===
|
||||||
|
if len(df_full.columns) < 2:
|
||||||
|
raise ValueError("DataFrame должен содержать как минимум 2 столбца.")
|
||||||
|
|
||||||
|
new_columns = df_full.columns.tolist()
|
||||||
|
|
||||||
|
new_columns[0] = 'name'
|
||||||
|
new_columns[1] = 'normativ'
|
||||||
|
new_columns[-2] = 'total'
|
||||||
|
new_columns[-1] = 'total_1'
|
||||||
|
|
||||||
|
df_full.columns = new_columns
|
||||||
|
|
||||||
|
# Проверяем, что колонка 'name' существует
|
||||||
|
if 'name' in df_full.columns:
|
||||||
|
# Применяем функцию get_id_by_name к каждой строке в колонке 'name'
|
||||||
|
df_full['id'] = df_full['name'].apply(get_object_by_name)
|
||||||
|
|
||||||
|
# Устанавливаем id как индекс
|
||||||
|
df_full.set_index('id', inplace=True)
|
||||||
|
print(f"Окончательное количество столбцов: {len(df_full.columns)}")
|
||||||
|
return df_full
|
||||||
|
|
||||||
|
def parse(self, file_path: str, params: dict) -> dict:
|
||||||
|
import zipfile
|
||||||
df_monitorings = {} # ЭТО СЛОВАРЬ ДАТАФРЕЙМОВ, ГДЕ КЛЮЧ - НОМЕР МЕСЯЦА, ЗНАЧЕНИЕ - ДАТАФРЕЙМ
|
df_monitorings = {} # ЭТО СЛОВАРЬ ДАТАФРЕЙМОВ, ГДЕ КЛЮЧ - НОМЕР МЕСЯЦА, ЗНАЧЕНИЕ - ДАТАФРЕЙМ
|
||||||
|
|
||||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||||
|
|
||||||
file_list = zip_ref.namelist()
|
file_list = zip_ref.namelist()
|
||||||
for month in range(1, 13):
|
for month in range(1, 13):
|
||||||
@@ -87,70 +103,7 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
|
|
||||||
return df_monitorings
|
return df_monitorings
|
||||||
|
|
||||||
def find_header_row(self, file_path: str, sheet: str, search_value: str = "Установка", max_rows: int = 50) -> int:
|
def aggregate_by_columns(self, df_dict: Dict[str, pd.DataFrame], columns):
|
||||||
"""Определение индекса заголовка в Excel по ключевому слову"""
|
|
||||||
# Читаем первые max_rows строк без заголовков
|
|
||||||
df_temp = pd.read_excel(
|
|
||||||
file_path,
|
|
||||||
sheet_name=sheet,
|
|
||||||
header=None,
|
|
||||||
nrows=max_rows,
|
|
||||||
engine='openpyxl'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
|
||||||
for idx, row in df_temp.iterrows():
|
|
||||||
if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
|
|
||||||
print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
|
|
||||||
return idx + 1 # возвращаем индекс строки (0-based)
|
|
||||||
|
|
||||||
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
|
||||||
|
|
||||||
def parse_single(self, file, sheet, header_num=None):
|
|
||||||
''' Собственно парсер отчетов одного объекта'''
|
|
||||||
# Автоопределение header_num, если не передан
|
|
||||||
if header_num is None:
|
|
||||||
header_num = self.find_header_row(file, sheet, search_value="Установка")
|
|
||||||
# Читаем весь лист, начиная с найденной строки как заголовок
|
|
||||||
df_full = pd.read_excel(
|
|
||||||
file,
|
|
||||||
sheet_name=sheet,
|
|
||||||
header=header_num,
|
|
||||||
usecols=None,
|
|
||||||
index_col=None,
|
|
||||||
engine='openpyxl'
|
|
||||||
)
|
|
||||||
|
|
||||||
# === Удаление полностью пустых столбцов ===
|
|
||||||
df_clean = df_full.replace(r'^\s*$', pd.NA, regex=True) # заменяем пустые строки на NA
|
|
||||||
df_clean = df_clean.dropna(axis=1, how='all') # удаляем столбцы, где все значения — NA
|
|
||||||
df_full = df_full.loc[:, df_clean.columns] # оставляем только непустые столбцы
|
|
||||||
|
|
||||||
# === Переименовываем нужные столбцы по позициям ===
|
|
||||||
if len(df_full.columns) < 2:
|
|
||||||
raise ValueError("DataFrame должен содержать как минимум 2 столбца.")
|
|
||||||
|
|
||||||
new_columns = df_full.columns.tolist()
|
|
||||||
|
|
||||||
new_columns[0] = 'name'
|
|
||||||
new_columns[1] = 'normativ'
|
|
||||||
new_columns[-2] = 'total'
|
|
||||||
new_columns[-1] = 'total_1'
|
|
||||||
|
|
||||||
df_full.columns = new_columns
|
|
||||||
|
|
||||||
# Проверяем, что колонка 'name' существует
|
|
||||||
if 'name' in df_full.columns:
|
|
||||||
# Применяем функцию get_id_by_name к каждой строке в колонке 'name'
|
|
||||||
# df_full['id'] = df_full['name'].apply(get_object_by_name) # This line was removed as per new_code
|
|
||||||
pass # Placeholder for new_code
|
|
||||||
|
|
||||||
# Устанавливаем id как индекс
|
|
||||||
df_full.set_index('id', inplace=True)
|
|
||||||
print(f"Окончательное количество столбцов: {len(df_full.columns)}")
|
|
||||||
return df_full
|
|
||||||
|
|
||||||
def aggregate_by_columns(self, df_dict: Dict[str, pd.DataFrame], columns: list) -> Tuple[pd.DataFrame, Dict[str, pd.DataFrame]]:
|
|
||||||
''' Служебная функция. Агрегация данных по среднему по определенным колонкам. '''
|
''' Служебная функция. Агрегация данных по среднему по определенным колонкам. '''
|
||||||
all_data = {} # Для хранения полных данных (месяцы) по каждой колонке
|
all_data = {} # Для хранения полных данных (месяцы) по каждой колонке
|
||||||
means = {} # Для хранения средних
|
means = {} # Для хранения средних
|
||||||
@@ -232,3 +185,22 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
total.name = 'mean'
|
total.name = 'mean'
|
||||||
|
|
||||||
return total, df_combined
|
return total, df_combined
|
||||||
|
|
||||||
|
def get_value(self, df, params):
|
||||||
|
mode = params.get("mode", "total")
|
||||||
|
columns = params.get("columns", None)
|
||||||
|
month = params.get("month", None)
|
||||||
|
data = None
|
||||||
|
if mode == "total":
|
||||||
|
if not columns:
|
||||||
|
raise ValueError("Отсутствуют идентификаторы столбцов")
|
||||||
|
df_means, _ = self.aggregate_by_columns(df, columns)
|
||||||
|
data = df_means.to_dict(orient='index')
|
||||||
|
elif mode == "month":
|
||||||
|
if not month:
|
||||||
|
raise ValueError("Отсутствуют идентификатор месяца")
|
||||||
|
df_month = self.get_month(df, month)
|
||||||
|
data = df_month.to_dict(orient='index')
|
||||||
|
|
||||||
|
json_result = data_to_json(data)
|
||||||
|
return json_result
|
||||||
|
|||||||
@@ -6,156 +6,13 @@ from adapters.pconfig import get_og_by_name
|
|||||||
|
|
||||||
|
|
||||||
class SvodkaCAParser(ParserPort):
|
class SvodkaCAParser(ParserPort):
|
||||||
"""Парсер для сводок СА"""
|
"""Парсер для сводки СА"""
|
||||||
|
|
||||||
name = "Сводки СА"
|
name = "Сводка СА"
|
||||||
|
|
||||||
def _register_default_getters(self):
|
|
||||||
"""Регистрация геттеров по умолчанию"""
|
|
||||||
self.register_getter(
|
|
||||||
name="get_data",
|
|
||||||
method=self._get_data_wrapper,
|
|
||||||
required_params=["modes", "tables"],
|
|
||||||
optional_params=[],
|
|
||||||
description="Получение данных по режимам и таблицам"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_data_wrapper(self, params: dict):
|
|
||||||
"""Обертка для получения данных (для совместимости)"""
|
|
||||||
modes = params["modes"]
|
|
||||||
tables = params["tables"]
|
|
||||||
|
|
||||||
if not isinstance(modes, list):
|
|
||||||
raise ValueError("Поле 'modes' должно быть списком")
|
|
||||||
if not isinstance(tables, list):
|
|
||||||
raise ValueError("Поле 'tables' должно быть списком")
|
|
||||||
|
|
||||||
# TODO: Переделать под новую архитектуру
|
|
||||||
data_dict = {}
|
|
||||||
for mode in modes:
|
|
||||||
data_dict[mode] = self.get_data(self.df, mode, tables)
|
|
||||||
return self.data_dict_to_json(data_dict)
|
|
||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
|
||||||
"""Парсинг файла и возврат DataFrame"""
|
|
||||||
# Сохраняем DataFrame для использования в геттерах
|
|
||||||
self.df = self.parse_svodka_ca(file_path, params)
|
|
||||||
return self.df
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Фильтруем таблицы: оставляем только те, где первая строка содержит нужные заголовки
|
|
||||||
filtered_tables = []
|
|
||||||
for table in tables:
|
|
||||||
if table.empty:
|
|
||||||
continue
|
|
||||||
first_row_values = table.iloc[0].astype(str).str.strip().tolist()
|
|
||||||
if any(val in inclusion_list for val in first_row_values):
|
|
||||||
filtered_tables.append(table)
|
|
||||||
|
|
||||||
tables = filtered_tables
|
|
||||||
|
|
||||||
# === Итоговый список таблиц датафреймов ===
|
|
||||||
result_list = []
|
|
||||||
|
|
||||||
for table in tables:
|
|
||||||
if table.empty:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Получаем первую строку (до удаления)
|
|
||||||
first_row_values = table.iloc[0].astype(str).str.strip().tolist()
|
|
||||||
|
|
||||||
# Находим, какой элемент из inclusion_list присутствует
|
|
||||||
matched_key = None
|
|
||||||
for val in first_row_values:
|
|
||||||
if val in inclusion_list:
|
|
||||||
matched_key = val
|
|
||||||
break # берём первый совпадающий заголовок
|
|
||||||
|
|
||||||
if matched_key is None:
|
|
||||||
continue # на всякий случай (хотя уже отфильтровано)
|
|
||||||
|
|
||||||
# Удаляем первую строку (заголовок) и сбрасываем индекс
|
|
||||||
df_cleaned = table.iloc[1:].copy().reset_index(drop=True)
|
|
||||||
|
|
||||||
# Пропускаем, если таблица пустая
|
|
||||||
if df_cleaned.empty:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Первая строка становится заголовком
|
|
||||||
new_header = df_cleaned.iloc[0] # извлекаем первую строку как потенциальные названия столбцов
|
|
||||||
|
|
||||||
# Преобразуем заголовок: только первый столбец может быть заменён на "name"
|
|
||||||
cleaned_header = []
|
|
||||||
|
|
||||||
# Обрабатываем первый столбец отдельно
|
|
||||||
first_item = new_header.iloc[0] if isinstance(new_header, pd.Series) else new_header[0]
|
|
||||||
first_item_str = str(first_item).strip() if pd.notna(first_item) else ""
|
|
||||||
if first_item_str == "" or first_item_str == "nan":
|
|
||||||
cleaned_header.append("name")
|
|
||||||
else:
|
|
||||||
cleaned_header.append(first_item_str)
|
|
||||||
|
|
||||||
# Остальные столбцы добавляем без изменений (или с минимальной очисткой)
|
|
||||||
for item in new_header[1:]:
|
|
||||||
# Опционально: приводим к строке и убираем лишние пробелы, но не заменяем на "name"
|
|
||||||
item_str = str(item).strip() if pd.notna(item) else ""
|
|
||||||
cleaned_header.append(item_str)
|
|
||||||
|
|
||||||
# Применяем очищенные названия столбцов
|
|
||||||
df_cleaned = df_cleaned[1:] # удаляем строку с заголовком
|
|
||||||
df_cleaned.columns = cleaned_header
|
|
||||||
df_cleaned = df_cleaned.reset_index(drop=True)
|
|
||||||
|
|
||||||
if matched_key.endswith('**'):
|
|
||||||
cleaned_key = matched_key[:-2] # удаляем последние **
|
|
||||||
else:
|
|
||||||
cleaned_key = matched_key
|
|
||||||
|
|
||||||
# Добавляем новую колонку с именем параметра
|
|
||||||
df_cleaned["table"] = cleaned_key
|
|
||||||
|
|
||||||
# Проверяем, что колонка 'name' существует
|
|
||||||
if 'name' not in df_cleaned.columns:
|
|
||||||
print(
|
|
||||||
f"Внимание: колонка 'name' отсутствует в таблице для '{matched_key}'. Пропускаем добавление 'id'.")
|
|
||||||
continue # или обработать по-другому
|
|
||||||
else:
|
|
||||||
# Применяем функцию get_id_by_name к каждой строке в колонке 'name'
|
|
||||||
df_cleaned['id'] = df_cleaned['name'].apply(get_og_by_name)
|
|
||||||
|
|
||||||
# Удаляем строки, где id — None, NaN или пустой
|
|
||||||
df_cleaned = df_cleaned.dropna(subset=['id']) # dropna удаляет NaN
|
|
||||||
# Дополнительно: удаляем None (если не поймал dropna)
|
|
||||||
df_cleaned = df_cleaned[df_cleaned['id'].notna() & (df_cleaned['id'].astype(str) != 'None')]
|
|
||||||
|
|
||||||
# Добавляем в словарь
|
|
||||||
result_list.append(df_cleaned)
|
|
||||||
|
|
||||||
# === Объединение и сортировка по id (индекс) и table ===
|
|
||||||
if result_list:
|
|
||||||
combined_df = pd.concat(result_list, axis=0)
|
|
||||||
|
|
||||||
# Сортируем по индексу (id) и по столбцу 'table'
|
|
||||||
combined_df = combined_df.sort_values(by=['id', 'table'], axis=0)
|
|
||||||
|
|
||||||
# Устанавливаем id как индекс
|
|
||||||
# combined_df.set_index('id', inplace=True)
|
|
||||||
|
|
||||||
return combined_df
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def extract_all_tables(self, file_path, sheet_name=0):
|
def extract_all_tables(self, file_path, sheet_name=0):
|
||||||
"""Извлечение всех таблиц из Excel файла"""
|
"""Извлекает все таблицы из Excel файла"""
|
||||||
df = pd.read_excel(file_path, sheet_name=sheet_name, header=None, engine='openpyxl')
|
df = pd.read_excel(file_path, sheet_name=sheet_name, header=None)
|
||||||
df_filled = df.fillna('')
|
df_filled = df.fillna('')
|
||||||
df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True)
|
df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True)
|
||||||
|
|
||||||
@@ -226,8 +83,8 @@ class SvodkaCAParser(ParserPort):
|
|||||||
return None
|
return None
|
||||||
return name.strip()
|
return name.strip()
|
||||||
|
|
||||||
def parse_sheet(self, file_path: str, sheet_name: str, inclusion_list: set) -> pd.DataFrame:
|
def parse_sheet(self, file_path, sheet_name, inclusion_list):
|
||||||
"""Парсинг листа Excel"""
|
"""Собственно функция парсинга отчета СА"""
|
||||||
# === Извлечение и фильтрация ===
|
# === Извлечение и фильтрация ===
|
||||||
tables = self.extract_all_tables(file_path, sheet_name)
|
tables = self.extract_all_tables(file_path, sheet_name)
|
||||||
|
|
||||||
@@ -333,6 +190,77 @@ class SvodkaCAParser(ParserPort):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def parse(self, file_path: str, params: dict) -> dict:
|
||||||
|
"""Парсинг файла сводки СА"""
|
||||||
|
# === Точка входа. Нужно выгрузить три таблицы: План, Факт и Норматив ===
|
||||||
|
# Выгружаем План в df_ca_plan
|
||||||
|
inclusion_list_plan = {
|
||||||
|
"ТиП, %",
|
||||||
|
"Топливо итого, тонн",
|
||||||
|
"Топливо итого, %",
|
||||||
|
"Топливо на технологию, тонн",
|
||||||
|
"Топливо на технологию, %",
|
||||||
|
"Топливо на энергетику, тонн",
|
||||||
|
"Топливо на энергетику, %",
|
||||||
|
"Потери итого, тонн",
|
||||||
|
"Потери итого, %",
|
||||||
|
"в т.ч. Идентифицированные безвозвратные потери, тонн**",
|
||||||
|
"в т.ч. Идентифицированные безвозвратные потери, %**",
|
||||||
|
"в т.ч. Неидентифицированные потери, тонн**",
|
||||||
|
"в т.ч. Неидентифицированные потери, %**"
|
||||||
|
}
|
||||||
|
|
||||||
|
df_ca_plan = self.parse_sheet(file_path, 'План', inclusion_list_plan) # ЭТО ДАТАФРЕЙМ ПЛАНА В СВОДКЕ ЦА
|
||||||
|
print(f"\n--- Объединённый и отсортированный План: {df_ca_plan.shape} ---")
|
||||||
|
|
||||||
|
# Выгружаем Факт
|
||||||
|
inclusion_list_fact = {
|
||||||
|
"ТиП, %",
|
||||||
|
"Топливо итого, тонн",
|
||||||
|
"Топливо итого, %",
|
||||||
|
"Топливо на технологию, тонн",
|
||||||
|
"Топливо на технологию, %",
|
||||||
|
"Топливо на энергетику, тонн",
|
||||||
|
"Топливо на энергетику, %",
|
||||||
|
"Потери итого, тонн",
|
||||||
|
"Потери итого, %",
|
||||||
|
"в т.ч. Идентифицированные безвозвратные потери, тонн",
|
||||||
|
"в т.ч. Идентифицированные безвозвратные потери, %",
|
||||||
|
"в т.ч. Неидентифицированные потери, тонн",
|
||||||
|
"в т.ч. Неидентифицированные потери, %"
|
||||||
|
}
|
||||||
|
|
||||||
|
df_ca_fact = self.parse_sheet(file_path, 'Факт', inclusion_list_fact) # ЭТО ДАТАФРЕЙМ ФАКТА В СВОДКЕ ЦА
|
||||||
|
print(f"\n--- Объединённый и отсортированный Факт: {df_ca_fact.shape} ---")
|
||||||
|
|
||||||
|
# Выгружаем План в df_ca_normativ
|
||||||
|
inclusion_list_normativ = {
|
||||||
|
"Топливо итого, тонн",
|
||||||
|
"Топливо итого, %",
|
||||||
|
"Топливо на технологию, тонн",
|
||||||
|
"Топливо на технологию, %",
|
||||||
|
"Топливо на энергетику, тонн",
|
||||||
|
"Топливо на энергетику, %",
|
||||||
|
"Потери итого, тонн",
|
||||||
|
"Потери итого, %",
|
||||||
|
"в т.ч. Идентифицированные безвозвратные потери, тонн**",
|
||||||
|
"в т.ч. Идентифицированные безвозвратные потери, %**",
|
||||||
|
"в т.ч. Неидентифицированные потери, тонн**",
|
||||||
|
"в т.ч. Неидентифицированные потери, %**"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ЭТО ДАТАФРЕЙМ НОРМАТИВА В СВОДКЕ ЦА
|
||||||
|
df_ca_normativ = self.parse_sheet(file_path, 'Норматив', inclusion_list_normativ)
|
||||||
|
|
||||||
|
print(f"\n--- Объединённый и отсортированный Норматив: {df_ca_normativ.shape} ---")
|
||||||
|
|
||||||
|
df_dict = {
|
||||||
|
"plan": df_ca_plan,
|
||||||
|
"fact": df_ca_fact,
|
||||||
|
"normativ": df_ca_normativ
|
||||||
|
}
|
||||||
|
return df_dict
|
||||||
|
|
||||||
def data_dict_to_json(self, data_dict):
|
def data_dict_to_json(self, data_dict):
|
||||||
''' Служебная функция для парсинга словаря в json. '''
|
''' Служебная функция для парсинга словаря в json. '''
|
||||||
def convert_types(obj):
|
def convert_types(obj):
|
||||||
@@ -380,3 +308,17 @@ class SvodkaCAParser(ParserPort):
|
|||||||
filtered_df = df[df['table'].isin(table_values)].copy()
|
filtered_df = df[df['table'].isin(table_values)].copy()
|
||||||
result_dict = {key: group for key, group in filtered_df.groupby('table')}
|
result_dict = {key: group for key, group in filtered_df.groupby('table')}
|
||||||
return result_dict
|
return result_dict
|
||||||
|
|
||||||
|
def get_value(self, df: pd.DataFrame, params: dict):
|
||||||
|
|
||||||
|
modes = params.get("modes")
|
||||||
|
tables = params.get("tables")
|
||||||
|
if not isinstance(modes, list):
|
||||||
|
raise ValueError("Поле 'modes' должно быть списком")
|
||||||
|
if not isinstance(tables, list):
|
||||||
|
raise ValueError("Поле 'tables' должно быть списком")
|
||||||
|
# Собираем данные
|
||||||
|
data_dict = {}
|
||||||
|
for mode in modes:
|
||||||
|
data_dict[mode] = self.get_data(df, mode, tables)
|
||||||
|
return self.data_dict_to_json(data_dict)
|
||||||
|
|||||||
@@ -9,60 +9,6 @@ class SvodkaPMParser(ParserPort):
|
|||||||
|
|
||||||
name = "Сводки ПМ"
|
name = "Сводки ПМ"
|
||||||
|
|
||||||
def _register_default_getters(self):
|
|
||||||
"""Регистрация геттеров по умолчанию"""
|
|
||||||
self.register_getter(
|
|
||||||
name="single_og",
|
|
||||||
method=self._get_single_og,
|
|
||||||
required_params=["id", "codes", "columns"],
|
|
||||||
optional_params=["search"],
|
|
||||||
description="Получение данных по одному ОГ"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.register_getter(
|
|
||||||
name="total_ogs",
|
|
||||||
method=self._get_total_ogs,
|
|
||||||
required_params=["codes", "columns"],
|
|
||||||
optional_params=["search"],
|
|
||||||
description="Получение данных по всем ОГ"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_single_og(self, params: dict):
|
|
||||||
"""Получение данных по одному ОГ (обертка для совместимости)"""
|
|
||||||
og_id = params["id"]
|
|
||||||
codes = params["codes"]
|
|
||||||
columns = params["columns"]
|
|
||||||
search = params.get("search")
|
|
||||||
|
|
||||||
if not isinstance(codes, list):
|
|
||||||
raise ValueError("Поле 'codes' должно быть списком")
|
|
||||||
if not isinstance(columns, list):
|
|
||||||
raise ValueError("Поле 'columns' должно быть списком")
|
|
||||||
|
|
||||||
# Здесь нужно получить DataFrame из self.df, но пока используем старую логику
|
|
||||||
# TODO: Переделать под новую архитектуру
|
|
||||||
return self.get_svodka_og(self.df, og_id, codes, columns, search)
|
|
||||||
|
|
||||||
def _get_total_ogs(self, params: dict):
|
|
||||||
"""Получение данных по всем ОГ (обертка для совместимости)"""
|
|
||||||
codes = params["codes"]
|
|
||||||
columns = params["columns"]
|
|
||||||
search = params.get("search")
|
|
||||||
|
|
||||||
if not isinstance(codes, list):
|
|
||||||
raise ValueError("Поле 'codes' должно быть списком")
|
|
||||||
if not isinstance(columns, list):
|
|
||||||
raise ValueError("Поле 'columns' должно быть списком")
|
|
||||||
|
|
||||||
# TODO: Переделать под новую архитектуру
|
|
||||||
return self.get_svodka_total(self.df, codes, columns, search)
|
|
||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
|
||||||
"""Парсинг файла и возврат DataFrame"""
|
|
||||||
# Сохраняем DataFrame для использования в геттерах
|
|
||||||
self.df = self.parse_svodka_pm_files(file_path, params)
|
|
||||||
return self.df
|
|
||||||
|
|
||||||
def find_header_row(self, file: str, sheet: str, search_value: str = "Итого", max_rows: int = 50) -> int:
|
def find_header_row(self, file: str, sheet: str, search_value: str = "Итого", max_rows: int = 50) -> int:
|
||||||
"""Определения индекса заголовка в excel по ключевому слову"""
|
"""Определения индекса заголовка в excel по ключевому слову"""
|
||||||
# Читаем первые max_rows строк без заголовков
|
# Читаем первые max_rows строк без заголовков
|
||||||
@@ -70,8 +16,7 @@ class SvodkaPMParser(ParserPort):
|
|||||||
file,
|
file,
|
||||||
sheet_name=sheet,
|
sheet_name=sheet,
|
||||||
header=None,
|
header=None,
|
||||||
nrows=max_rows,
|
nrows=max_rows
|
||||||
engine='openpyxl'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
||||||
@@ -95,7 +40,6 @@ class SvodkaPMParser(ParserPort):
|
|||||||
header=header_num,
|
header=header_num,
|
||||||
usecols=None,
|
usecols=None,
|
||||||
nrows=2,
|
nrows=2,
|
||||||
engine='openpyxl'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if df_probe.shape[0] == 0:
|
if df_probe.shape[0] == 0:
|
||||||
@@ -117,8 +61,7 @@ class SvodkaPMParser(ParserPort):
|
|||||||
sheet_name=sheet,
|
sheet_name=sheet,
|
||||||
header=header_num,
|
header=header_num,
|
||||||
usecols=None,
|
usecols=None,
|
||||||
index_col=None,
|
index_col=None
|
||||||
engine='openpyxl'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if indicator_col_name not in df_full.columns:
|
if indicator_col_name not in df_full.columns:
|
||||||
@@ -156,25 +99,25 @@ class SvodkaPMParser(ParserPort):
|
|||||||
# Проверяем, является ли колонка пустой/некорректной
|
# Проверяем, является ли колонка пустой/некорректной
|
||||||
is_empty_or_unnamed = col_str.startswith('Unnamed') or col_str == '' or col_str.lower() == 'nan'
|
is_empty_or_unnamed = col_str.startswith('Unnamed') or col_str == '' or col_str.lower() == 'nan'
|
||||||
|
|
||||||
if is_empty_or_unnamed:
|
# Проверяем, начинается ли на "Итого"
|
||||||
# Если это пустая колонка, используем последнее хорошее имя
|
if col_str.startswith('Итого'):
|
||||||
if last_good_name:
|
current_name = 'Итого'
|
||||||
new_columns.append(last_good_name)
|
last_good_name = current_name # обновляем last_good_name
|
||||||
else:
|
new_columns.append(current_name)
|
||||||
# Если нет хорошего имени, пропускаем
|
elif is_empty_or_unnamed:
|
||||||
continue
|
# Используем последнее хорошее имя
|
||||||
|
new_columns.append(last_good_name)
|
||||||
else:
|
else:
|
||||||
# Это хорошая колонка
|
# Имя, полученное из exel
|
||||||
last_good_name = col_str
|
last_good_name = col_str
|
||||||
new_columns.append(col_str)
|
new_columns.append(col_str)
|
||||||
|
|
||||||
# Применяем новые заголовки
|
|
||||||
df_final.columns = new_columns
|
df_final.columns = new_columns
|
||||||
|
|
||||||
|
print(f"Окончательное количество столбцов: {len(df_final.columns)}")
|
||||||
return df_final
|
return df_final
|
||||||
|
|
||||||
def parse_svodka_pm_files(self, zip_path: str, params: dict) -> dict:
|
def parse(self, file_path: str, params: dict) -> dict:
|
||||||
"""Парсинг ZIP архива со сводками ПМ"""
|
|
||||||
import zipfile
|
import zipfile
|
||||||
pm_dict = {
|
pm_dict = {
|
||||||
"facts": {},
|
"facts": {},
|
||||||
@@ -182,7 +125,7 @@ class SvodkaPMParser(ParserPort):
|
|||||||
}
|
}
|
||||||
excel_fact_template = 'svodka_fact_pm_ID.xlsm'
|
excel_fact_template = 'svodka_fact_pm_ID.xlsm'
|
||||||
excel_plan_template = 'svodka_plan_pm_ID.xlsx'
|
excel_plan_template = 'svodka_plan_pm_ID.xlsx'
|
||||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||||
file_list = zip_ref.namelist()
|
file_list = zip_ref.namelist()
|
||||||
for name, id in OG_IDS.items():
|
for name, id in OG_IDS.items():
|
||||||
if id == 'BASH':
|
if id == 'BASH':
|
||||||
@@ -212,9 +155,9 @@ class SvodkaPMParser(ParserPort):
|
|||||||
|
|
||||||
return pm_dict
|
return pm_dict
|
||||||
|
|
||||||
def get_svodka_value(self, df_svodka, code, search_value, search_value_filter=None):
|
def get_svodka_value(self, df_svodka, id, code, search_value=None):
|
||||||
''' Служебная функция получения значения по коду и столбцу '''
|
''' Служебная функция для простой выборке по сводке '''
|
||||||
row_index = code
|
row_index = id
|
||||||
|
|
||||||
mask_value = df_svodka.iloc[0] == code
|
mask_value = df_svodka.iloc[0] == code
|
||||||
if search_value is None:
|
if search_value is None:
|
||||||
@@ -311,4 +254,22 @@ class SvodkaPMParser(ParserPort):
|
|||||||
|
|
||||||
return total_result
|
return total_result
|
||||||
|
|
||||||
# Убираем старый метод get_value, так как он теперь в базовом классе
|
def get_value(self, df, params):
|
||||||
|
og_id = params.get("id")
|
||||||
|
codes = params.get("codes")
|
||||||
|
columns = params.get("columns")
|
||||||
|
search = params.get("search")
|
||||||
|
mode = params.get("mode", "total")
|
||||||
|
if not isinstance(codes, list):
|
||||||
|
raise ValueError("Поле 'codes' должно быть списком")
|
||||||
|
if not isinstance(columns, list):
|
||||||
|
raise ValueError("Поле 'columns' должно быть списком")
|
||||||
|
data = None
|
||||||
|
if mode == "single":
|
||||||
|
if not og_id:
|
||||||
|
raise ValueError("Отсутствует идентификатор ОГ")
|
||||||
|
data = self.get_svodka_og(df, og_id, codes, columns, search)
|
||||||
|
elif mode == "total":
|
||||||
|
data = self.get_svodka_total(df, codes, columns, search)
|
||||||
|
json_result = data_to_json(data)
|
||||||
|
return json_result
|
||||||
|
|||||||
@@ -11,5 +11,4 @@ requests>=2.31.0
|
|||||||
# pytest-cov>=4.0.0
|
# pytest-cov>=4.0.0
|
||||||
# pytest-mock>=3.10.0
|
# pytest-mock>=3.10.0
|
||||||
httpx>=0.24.0
|
httpx>=0.24.0
|
||||||
numpy
|
numpy
|
||||||
streamlit>=1.28.0
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
python-3.11.*
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Запуск Streamlit интерфейса локально из изолированного пакета
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import webbrowser
|
|
||||||
import os
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Основная функция"""
|
|
||||||
print("🚀 ЗАПУСК STREAMLIT ИЗ ИЗОЛИРОВАННОГО ПАКЕТА")
|
|
||||||
print("=" * 60)
|
|
||||||
print("Убедитесь, что FastAPI сервер запущен на порту 8000")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Проверяем, существует ли папка streamlit_app
|
|
||||||
if not os.path.exists("streamlit_app"):
|
|
||||||
print("❌ Папка streamlit_app не найдена")
|
|
||||||
print("Создайте изолированный пакет или используйте docker-compose up -d")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Переходим в папку streamlit_app
|
|
||||||
os.chdir("streamlit_app")
|
|
||||||
|
|
||||||
# Проверяем, установлен ли Streamlit
|
|
||||||
try:
|
|
||||||
import streamlit
|
|
||||||
print(f"✅ Streamlit {streamlit.__version__} установлен")
|
|
||||||
except ImportError:
|
|
||||||
print("❌ Streamlit не установлен")
|
|
||||||
print("Установите: pip install -r requirements.txt")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("\n🚀 Запускаю Streamlit...")
|
|
||||||
print("📍 URL: http://localhost:8501")
|
|
||||||
print("🔗 API: http://localhost:8000")
|
|
||||||
print("🛑 Для остановки нажмите Ctrl+C")
|
|
||||||
|
|
||||||
# Открываем браузер
|
|
||||||
try:
|
|
||||||
webbrowser.open("http://localhost:8501")
|
|
||||||
print("✅ Браузер открыт")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Не удалось открыть браузер: {e}")
|
|
||||||
|
|
||||||
# Запускаем 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",
|
|
||||||
"--server.port", "8501",
|
|
||||||
"--server.address", "localhost",
|
|
||||||
"--server.headless", "false",
|
|
||||||
"--browser.gatherUsageStats", "false"
|
|
||||||
], env=env)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\n👋 Streamlit остановлен")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
49
start_dev.py
Normal file
49
start_dev.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Скрипт для запуска проекта в режиме разработки
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def run_command(command, description):
|
||||||
|
"""Выполнение команды с выводом"""
|
||||||
|
print(f"🔄 {description}...")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
||||||
|
print(f"✅ {description} выполнено успешно")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"❌ Ошибка при {description.lower()}:")
|
||||||
|
print(f" Команда: {command}")
|
||||||
|
print(f" Ошибка: {e.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🚀 Запуск проекта в режиме разработки")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Останавливаем продакшн контейнеры если они запущены
|
||||||
|
if run_command("docker compose ps", "Проверка статуса контейнеров"):
|
||||||
|
if "Up" in subprocess.run("docker compose ps", shell=True, capture_output=True, text=True).stdout:
|
||||||
|
print("🛑 Останавливаю продакшн контейнеры...")
|
||||||
|
run_command("docker compose down", "Остановка продакшн контейнеров")
|
||||||
|
|
||||||
|
# Запускаем режим разработки
|
||||||
|
print("\n🔧 Запуск режима разработки...")
|
||||||
|
if run_command("docker compose -f docker-compose.dev.yml up -d", "Запуск контейнеров разработки"):
|
||||||
|
print("\n🎉 Проект запущен в режиме разработки!")
|
||||||
|
print("\n📍 Доступные сервисы:")
|
||||||
|
print(" • Streamlit: http://localhost:8501")
|
||||||
|
print(" • FastAPI: http://localhost:8000")
|
||||||
|
print(" • MinIO Console: http://localhost:9001")
|
||||||
|
print("\n💡 Теперь изменения в streamlit_app/ будут автоматически перезагружаться!")
|
||||||
|
print("\n🛑 Для остановки используйте:")
|
||||||
|
print(" docker compose -f docker-compose.dev.yml down")
|
||||||
|
else:
|
||||||
|
print("\n❌ Не удалось запустить проект в режиме разработки")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
49
start_prod.py
Normal file
49
start_prod.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Скрипт для запуска проекта в продакшн режиме
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def run_command(command, description):
|
||||||
|
"""Выполнение команды с выводом"""
|
||||||
|
print(f"🔄 {description}...")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
||||||
|
print(f"✅ {description} выполнено успешно")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"❌ Ошибка при {description.lower()}:")
|
||||||
|
print(f" Команда: {command}")
|
||||||
|
print(f" Ошибка: {e.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🚀 Запуск проекта в продакшн режиме")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Останавливаем контейнеры разработки если они запущены
|
||||||
|
if run_command("docker compose -f docker-compose.dev.yml ps", "Проверка статуса контейнеров разработки"):
|
||||||
|
if "Up" in subprocess.run("docker compose -f docker-compose.dev.yml ps", shell=True, capture_output=True, text=True).stdout:
|
||||||
|
print("🛑 Останавливаю контейнеры разработки...")
|
||||||
|
run_command("docker compose -f docker-compose.dev.yml down", "Остановка контейнеров разработки")
|
||||||
|
|
||||||
|
# Запускаем продакшн режим
|
||||||
|
print("\n🏭 Запуск продакшн режима...")
|
||||||
|
if run_command("docker compose up -d --build", "Запуск продакшн контейнеров"):
|
||||||
|
print("\n🎉 Проект запущен в продакшн режиме!")
|
||||||
|
print("\n📍 Доступные сервисы:")
|
||||||
|
print(" • Streamlit: http://localhost:8501")
|
||||||
|
print(" • FastAPI: http://localhost:8000")
|
||||||
|
print(" • MinIO Console: http://localhost:9001")
|
||||||
|
print("\n💡 Для разработки используйте:")
|
||||||
|
print(" python start_dev.py")
|
||||||
|
print("\n🛑 Для остановки используйте:")
|
||||||
|
print(" docker compose down")
|
||||||
|
else:
|
||||||
|
print("\n❌ Не удалось запустить проект в продакшн режиме")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
__pycache__
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
.Python
|
|
||||||
env
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
.tox
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.log
|
|
||||||
.git
|
|
||||||
.mypy_cache
|
|
||||||
.pytest_cache
|
|
||||||
.hypothesis
|
|
||||||
.DS_Store
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env/
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
15
streamlit_app/.streamlit/config.toml
Normal file
15
streamlit_app/.streamlit/config.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[server]
|
||||||
|
port = 8501
|
||||||
|
address = "0.0.0.0"
|
||||||
|
enableCORS = false
|
||||||
|
enableXsrfProtection = false
|
||||||
|
|
||||||
|
[browser]
|
||||||
|
gatherUsageStats = false
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
primaryColor = "#FF4B4B"
|
||||||
|
backgroundColor = "#FFFFFF"
|
||||||
|
secondaryBackgroundColor = "#F0F2F6"
|
||||||
|
textColor = "#262730"
|
||||||
|
font = "sans serif"
|
||||||
@@ -2,22 +2,22 @@ FROM python:3.11-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Устанавливаем системные зависимости
|
# Установка системных зависимостей
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
gcc \
|
gcc \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Копируем файлы зависимостей
|
# Копирование requirements.txt
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
|
|
||||||
# Устанавливаем Python зависимости
|
# Установка Python зависимостей
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Копируем код приложения
|
# Копирование кода приложения
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Открываем порт
|
# Открытие порта
|
||||||
EXPOSE 8501
|
EXPOSE 8501
|
||||||
|
|
||||||
# Команда запуска
|
# Запуск Streamlit
|
||||||
CMD ["streamlit", "run", "app.py", "--server.port", "8501", "--server.address", "0.0.0.0"]
|
CMD ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# 📊 Streamlit App - NIN Excel Parsers API
|
|
||||||
|
|
||||||
Изолированное Streamlit приложение для демонстрации работы NIN Excel Parsers API.
|
|
||||||
|
|
||||||
## 🚀 Запуск
|
|
||||||
|
|
||||||
### Локально:
|
|
||||||
```bash
|
|
||||||
cd streamlit_app
|
|
||||||
pip install -r requirements.txt
|
|
||||||
streamlit run app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### В Docker:
|
|
||||||
```bash
|
|
||||||
docker build -t streamlit-app .
|
|
||||||
docker run -p 8501:8501 streamlit-app
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Конфигурация
|
|
||||||
|
|
||||||
### Переменные окружения:
|
|
||||||
- `API_BASE_URL` - адрес FastAPI сервера (по умолчанию: `http://fastapi:8000`)
|
|
||||||
|
|
||||||
### Параметры Streamlit:
|
|
||||||
- Порт: 8501
|
|
||||||
- Адрес: 0.0.0.0 (для Docker)
|
|
||||||
- Режим: headless (для Docker)
|
|
||||||
|
|
||||||
## 📁 Структура
|
|
||||||
|
|
||||||
```
|
|
||||||
streamlit_app/
|
|
||||||
├── app.py # Основное приложение
|
|
||||||
├── requirements.txt # Зависимости Python
|
|
||||||
├── Dockerfile # Docker образ
|
|
||||||
├── .streamlit/ # Конфигурация Streamlit
|
|
||||||
│ └── config.toml # Настройки
|
|
||||||
└── README.md # Документация
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 Доступ
|
|
||||||
|
|
||||||
После запуска приложение доступно по адресу: **http://localhost:8501**
|
|
||||||
100
streamlit_app/_streamlit_app.py
Normal file
100
streamlit_app/_streamlit_app.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import streamlit as st
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import plotly.express as px
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
from minio import Minio
|
||||||
|
import os
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
# Конфигурация страницы
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="Сводка данных",
|
||||||
|
page_icon="📊",
|
||||||
|
layout="wide",
|
||||||
|
initial_sidebar_state="expanded"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Заголовок приложения
|
||||||
|
st.title("📊 Анализ данных сводки")
|
||||||
|
st.markdown("---")
|
||||||
|
|
||||||
|
# Инициализация MinIO клиента
|
||||||
|
@st.cache_resource
|
||||||
|
def init_minio_client():
|
||||||
|
try:
|
||||||
|
client = Minio(
|
||||||
|
os.getenv("MINIO_ENDPOINT", "localhost:9000"),
|
||||||
|
access_key=os.getenv("MINIO_ACCESS_KEY", "minioadmin"),
|
||||||
|
secret_key=os.getenv("MINIO_SECRET_KEY", "minioadmin"),
|
||||||
|
secure=os.getenv("MINIO_SECURE", "false").lower() == "true"
|
||||||
|
)
|
||||||
|
return client
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Ошибка подключения к MinIO: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Боковая панель
|
||||||
|
with st.sidebar:
|
||||||
|
st.header("⚙️ Настройки")
|
||||||
|
|
||||||
|
# Выбор типа данных
|
||||||
|
data_type = st.selectbox(
|
||||||
|
"Тип данных",
|
||||||
|
["Мониторинг топлива", "Сводка ПМ", "Сводка ЦА"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Выбор периода
|
||||||
|
period = st.date_input(
|
||||||
|
"Период",
|
||||||
|
value=pd.Timestamp.now().date()
|
||||||
|
)
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
st.markdown("### 📈 Статистика")
|
||||||
|
st.info("Выберите тип данных для анализа")
|
||||||
|
|
||||||
|
# Основной контент
|
||||||
|
col1, col2 = st.columns([2, 1])
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.subheader(f"📋 {data_type}")
|
||||||
|
|
||||||
|
if data_type == "Мониторинг топлива":
|
||||||
|
st.info("Анализ данных мониторинга топлива")
|
||||||
|
# Здесь будет логика для работы с данными мониторинга топлива
|
||||||
|
|
||||||
|
elif data_type == "Сводка ПМ":
|
||||||
|
st.info("Анализ данных сводки ПМ")
|
||||||
|
# Здесь будет логика для работы с данными сводки ПМ
|
||||||
|
|
||||||
|
elif data_type == "Сводка ЦА":
|
||||||
|
st.info("Анализ данных сводки ЦА")
|
||||||
|
# Здесь будет логика для работы с данными сводки ЦА
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.subheader("📊 Быстрая статистика")
|
||||||
|
st.metric("Всего записей", "0")
|
||||||
|
st.metric("Активных", "0")
|
||||||
|
st.metric("Ошибок", "0")
|
||||||
|
|
||||||
|
# Нижняя панель
|
||||||
|
st.markdown("---")
|
||||||
|
st.subheader("🔍 Детальный анализ")
|
||||||
|
|
||||||
|
# Заглушка для графиков
|
||||||
|
placeholder = st.empty()
|
||||||
|
with placeholder.container():
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.write("📈 График 1")
|
||||||
|
# Здесь будет график
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.write("📊 График 2")
|
||||||
|
# Здесь будет график
|
||||||
|
|
||||||
|
# Футер
|
||||||
|
st.markdown("---")
|
||||||
|
st.markdown("**Разработано для анализа данных сводки** | v1.0.0")
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
streamlit>=1.28.0
|
streamlit>=1.28.0
|
||||||
requests>=2.31.0
|
pandas>=2.0.0
|
||||||
pandas>=1.5.0
|
numpy>=1.24.0
|
||||||
numpy>=1.24.0
|
plotly>=5.15.0
|
||||||
|
minio>=7.1.0
|
||||||
|
openpyxl>=3.1.0
|
||||||
|
xlrd>=2.0.1
|
||||||
@@ -15,17 +15,9 @@ st.set_page_config(
|
|||||||
initial_sidebar_state="expanded"
|
initial_sidebar_state="expanded"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Конфигурация API - автоматически определяем правильный адрес
|
# Конфигурация API
|
||||||
def get_api_base_url():
|
API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker
|
||||||
"""Автоматически определяет правильный адрес API"""
|
API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя
|
||||||
# Если запущено в 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():
|
def check_api_health():
|
||||||
"""Проверка доступности API"""
|
"""Проверка доступности API"""
|
||||||
@@ -45,16 +37,6 @@ def get_available_parsers():
|
|||||||
except:
|
except:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_parser_getters(parser_name: str):
|
|
||||||
"""Получение информации о геттерах парсера"""
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/getters")
|
|
||||||
if response.status_code == 200:
|
|
||||||
return response.json()
|
|
||||||
return {}
|
|
||||||
except:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_server_info():
|
def get_server_info():
|
||||||
"""Получение информации о сервере"""
|
"""Получение информации о сервере"""
|
||||||
try:
|
try:
|
||||||
@@ -92,7 +74,7 @@ def main():
|
|||||||
st.info("Убедитесь, что FastAPI сервер запущен")
|
st.info("Убедитесь, что FastAPI сервер запущен")
|
||||||
return
|
return
|
||||||
|
|
||||||
st.success(f"✅ API доступен по адресу {API_BASE_URL}")
|
st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}")
|
||||||
|
|
||||||
# Боковая панель с информацией
|
# Боковая панель с информацией
|
||||||
with st.sidebar:
|
with st.sidebar:
|
||||||
@@ -124,9 +106,6 @@ def main():
|
|||||||
with tab1:
|
with tab1:
|
||||||
st.header("📊 Сводки ПМ - Полный функционал")
|
st.header("📊 Сводки ПМ - Полный функционал")
|
||||||
|
|
||||||
# Получаем информацию о геттерах
|
|
||||||
getters_info = get_parser_getters("svodka_pm")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
# Секция загрузки файлов
|
||||||
st.subheader("📤 Загрузка файлов")
|
st.subheader("📤 Загрузка файлов")
|
||||||
uploaded_pm = st.file_uploader(
|
uploaded_pm = st.file_uploader(
|
||||||
@@ -155,15 +134,6 @@ def main():
|
|||||||
# Секция получения данных
|
# Секция получения данных
|
||||||
st.subheader("🔍 Получение данных")
|
st.subheader("🔍 Получение данных")
|
||||||
|
|
||||||
# Показываем доступные геттеры
|
|
||||||
if getters_info and "getters" in getters_info:
|
|
||||||
st.info("📋 Доступные геттеры:")
|
|
||||||
for getter_name, getter_info in getters_info["getters"].items():
|
|
||||||
st.write(f"• **{getter_name}**: {getter_info.get('description', 'Нет описания')}")
|
|
||||||
st.write(f" - Обязательные параметры: {', '.join(getter_info.get('required_params', []))}")
|
|
||||||
if getter_info.get('optional_params'):
|
|
||||||
st.write(f" - Необязательные параметры: {', '.join(getter_info['optional_params'])}")
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
@@ -195,13 +165,12 @@ def main():
|
|||||||
if codes and columns:
|
if codes and columns:
|
||||||
with st.spinner("Получаю данные..."):
|
with st.spinner("Получаю данные..."):
|
||||||
data = {
|
data = {
|
||||||
"getter": "single_og",
|
|
||||||
"id": og_id,
|
"id": og_id,
|
||||||
"codes": codes,
|
"codes": codes,
|
||||||
"columns": columns
|
"columns": columns
|
||||||
}
|
}
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_pm/get_data", data)
|
result, status = make_api_request("/svodka_pm/get_single_og", data)
|
||||||
|
|
||||||
if status == 200:
|
if status == 200:
|
||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
@@ -232,12 +201,11 @@ def main():
|
|||||||
if codes_total and columns_total:
|
if codes_total and columns_total:
|
||||||
with st.spinner("Получаю данные..."):
|
with st.spinner("Получаю данные..."):
|
||||||
data = {
|
data = {
|
||||||
"getter": "total_ogs",
|
|
||||||
"codes": codes_total,
|
"codes": codes_total,
|
||||||
"columns": columns_total
|
"columns": columns_total
|
||||||
}
|
}
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_pm/get_data", data)
|
result, status = make_api_request("/svodka_pm/get_total_ogs", data)
|
||||||
|
|
||||||
if status == 200:
|
if status == 200:
|
||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
@@ -251,9 +219,6 @@ def main():
|
|||||||
with tab2:
|
with tab2:
|
||||||
st.header("🏭 Сводки СА - Полный функционал")
|
st.header("🏭 Сводки СА - Полный функционал")
|
||||||
|
|
||||||
# Получаем информацию о геттерах
|
|
||||||
getters_info = get_parser_getters("svodka_ca")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
# Секция загрузки файлов
|
||||||
st.subheader("📤 Загрузка файлов")
|
st.subheader("📤 Загрузка файлов")
|
||||||
uploaded_ca = st.file_uploader(
|
uploaded_ca = st.file_uploader(
|
||||||
@@ -281,16 +246,7 @@ def main():
|
|||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# Секция получения данных
|
# Секция получения данных
|
||||||
st.subheader("<EFBFBD><EFBFBD> Получение данных")
|
st.subheader("🔍 Получение данных")
|
||||||
|
|
||||||
# Показываем доступные геттеры
|
|
||||||
if getters_info and "getters" in getters_info:
|
|
||||||
st.info("📋 Доступные геттеры:")
|
|
||||||
for getter_name, getter_info in getters_info["getters"].items():
|
|
||||||
st.write(f"• **{getter_name}**: {getter_info.get('description', 'Нет описания')}")
|
|
||||||
st.write(f" - Обязательные параметры: {', '.join(getter_info.get('required_params', []))}")
|
|
||||||
if getter_info.get('optional_params'):
|
|
||||||
st.write(f" - Необязательные параметры: {', '.join(getter_info['optional_params'])}")
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
@@ -317,7 +273,6 @@ def main():
|
|||||||
if modes and tables:
|
if modes and tables:
|
||||||
with st.spinner("Получаю данные..."):
|
with st.spinner("Получаю данные..."):
|
||||||
data = {
|
data = {
|
||||||
"getter": "get_data",
|
|
||||||
"modes": modes,
|
"modes": modes,
|
||||||
"tables": tables
|
"tables": tables
|
||||||
}
|
}
|
||||||
@@ -328,7 +283,7 @@ def main():
|
|||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
st.json(result)
|
st.json(result)
|
||||||
else:
|
else:
|
||||||
st.error(f"❌ Ошибка: {result.get('message', f'Неизвестная ошибка: {status}')}")
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
else:
|
else:
|
||||||
st.warning("⚠️ Выберите режимы и таблицы")
|
st.warning("⚠️ Выберите режимы и таблицы")
|
||||||
|
|
||||||
@@ -336,9 +291,6 @@ def main():
|
|||||||
with tab3:
|
with tab3:
|
||||||
st.header("⛽ Мониторинг топлива - Полный функционал")
|
st.header("⛽ Мониторинг топлива - Полный функционал")
|
||||||
|
|
||||||
# Получаем информацию о геттерах
|
|
||||||
getters_info = get_parser_getters("monitoring_fuel")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
# Секция загрузки файлов
|
||||||
st.subheader("📤 Загрузка файлов")
|
st.subheader("📤 Загрузка файлов")
|
||||||
uploaded_fuel = st.file_uploader(
|
uploaded_fuel = st.file_uploader(
|
||||||
@@ -367,15 +319,6 @@ def main():
|
|||||||
# Секция получения данных
|
# Секция получения данных
|
||||||
st.subheader("🔍 Получение данных")
|
st.subheader("🔍 Получение данных")
|
||||||
|
|
||||||
# Показываем доступные геттеры
|
|
||||||
if getters_info and "getters" in getters_info:
|
|
||||||
st.info("📋 Доступные геттеры:")
|
|
||||||
for getter_name, getter_info in getters_info["getters"].items():
|
|
||||||
st.write(f"• **{getter_name}**: {getter_info.get('description', 'Нет описания')}")
|
|
||||||
st.write(f" - Обязательные параметры: {', '.join(getter_info.get('required_params', []))}")
|
|
||||||
if getter_info.get('optional_params'):
|
|
||||||
st.write(f" - Необязательные параметры: {', '.join(getter_info['optional_params'])}")
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
@@ -392,11 +335,10 @@ def main():
|
|||||||
if columns_fuel:
|
if columns_fuel:
|
||||||
with st.spinner("Получаю данные..."):
|
with st.spinner("Получаю данные..."):
|
||||||
data = {
|
data = {
|
||||||
"getter": "total_by_columns",
|
|
||||||
"columns": columns_fuel
|
"columns": columns_fuel
|
||||||
}
|
}
|
||||||
|
|
||||||
result, status = make_api_request("/monitoring_fuel/get_data", data)
|
result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data)
|
||||||
|
|
||||||
if status == 200:
|
if status == 200:
|
||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
@@ -418,11 +360,10 @@ def main():
|
|||||||
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
|
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
|
||||||
with st.spinner("Получаю данные..."):
|
with st.spinner("Получаю данные..."):
|
||||||
data = {
|
data = {
|
||||||
"getter": "month_by_code",
|
|
||||||
"month": month
|
"month": month
|
||||||
}
|
}
|
||||||
|
|
||||||
result, status = make_api_request("/monitoring_fuel/get_data", data)
|
result, status = make_api_request("/monitoring_fuel/get_month_by_code", data)
|
||||||
|
|
||||||
if status == 200:
|
if status == 200:
|
||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
@@ -433,7 +374,7 @@ def main():
|
|||||||
# Футер
|
# Футер
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("### 📚 Документация API")
|
st.markdown("### 📚 Документация API")
|
||||||
st.markdown(f"Полная документация доступна по адресу: {API_BASE_URL}/docs")
|
st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs")
|
||||||
|
|
||||||
# Информация о проекте
|
# Информация о проекте
|
||||||
with st.expander("ℹ️ О проекте"):
|
with st.expander("ℹ️ О проекте"):
|
||||||
84
test_api.py
84
test_api.py
@@ -1,84 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
#!/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)
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
Reference in New Issue
Block a user