Compare commits
14 Commits
4cbdaf1b60
...
upd_exist_
| Author | SHA1 | Date | |
|---|---|---|---|
| de63f98b50 | |||
| 84069e4e41 | |||
| b8074765e3 | |||
| 79ab91c700 | |||
| b98be22359 | |||
| fc0b4356da | |||
| 72fe115a99 | |||
| 46a30c32ed | |||
| 5e217c7cce | |||
| 7d2747c8fe | |||
| 513ff3c144 | |||
| a0b6e04d99 | |||
| 47a7344755 | |||
| 456e9935f0 |
208
.gitignore
vendored
208
.gitignore
vendored
@@ -1,15 +1,12 @@
|
||||
data
|
||||
.streamlit
|
||||
# Python
|
||||
__pycache__
|
||||
*.pyc
|
||||
|
||||
nin_python_parser
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
@@ -23,88 +20,13 @@ parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
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
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
@@ -113,6 +35,86 @@ ENV/
|
||||
env.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
|
||||
.spyderproject
|
||||
.spyproject
|
||||
@@ -131,23 +133,27 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# Project specific
|
||||
data/
|
||||
*.zip
|
||||
*.xlsx
|
||||
*.xls
|
||||
*.xlsm
|
||||
# Local development
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# MinIO data directory
|
||||
minio_data/
|
||||
# FastAPI
|
||||
.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 сервер + парсеры Excel
|
||||
- **`streamlit_app/`** - Веб-интерфейс для демонстрации API
|
||||
Проект состоит из трех основных компонентов:
|
||||
- **python_parser** - FastAPI приложение для парсинга и обработки данных
|
||||
- **streamlit_app** - Streamlit приложение для визуализации и анализа
|
||||
- **minio_data** - хранилище данных MinIO
|
||||
|
||||
## 🚀 Быстрый запуск
|
||||
|
||||
### **Вариант 1: Все сервисы в Docker (рекомендуется)**
|
||||
```bash
|
||||
# Запуск всех сервисов: MinIO + FastAPI + Streamlit
|
||||
docker-compose up -d
|
||||
### Предварительные требования
|
||||
- Docker и Docker Compose
|
||||
- Git
|
||||
|
||||
# Доступ:
|
||||
# - MinIO Console: http://localhost:9001
|
||||
# - FastAPI: http://localhost:8000
|
||||
# - Streamlit: http://localhost:8501
|
||||
# - API Docs: http://localhost:8000/docs
|
||||
### Запуск всех сервисов (продакшн)
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### **Вариант 2: Только MinIO в Docker + сервисы локально**
|
||||
### Запуск в режиме разработки
|
||||
```bash
|
||||
# Запуск MinIO в Docker
|
||||
docker-compose up -d minio
|
||||
# Автоматический запуск
|
||||
python start_dev.py
|
||||
|
||||
# Запуск FastAPI локально
|
||||
cd python_parser
|
||||
python run_dev.py
|
||||
|
||||
# В отдельном терминале - Streamlit
|
||||
cd streamlit_app
|
||||
streamlit run app.py
|
||||
# Или вручную
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
### **Вариант 3: Только MinIO в Docker**
|
||||
**Режим разработки** позволяет:
|
||||
- Автоматически перезагружать Streamlit при изменении кода
|
||||
- Монтировать исходный код напрямую в контейнер
|
||||
- Видеть изменения без пересборки контейнеров
|
||||
|
||||
### Доступ к сервисам
|
||||
- **FastAPI**: http://localhost:8000
|
||||
- **Streamlit**: http://localhost:8501
|
||||
- **MinIO Console**: http://localhost:9001
|
||||
- **MinIO API**: http://localhost:9000
|
||||
|
||||
### Остановка сервисов
|
||||
```bash
|
||||
# Запуск только MinIO
|
||||
docker-compose up -d minio
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## 📋 Описание сервисов
|
||||
|
||||
- **MinIO** (порт 9000-9001): S3-совместимое хранилище для данных
|
||||
- **FastAPI** (порт 8000): API сервер для парсинга Excel файлов
|
||||
- **Streamlit** (порт 8501): Веб-интерфейс для демонстрации API
|
||||
|
||||
## 📁 Структура проекта
|
||||
|
||||
```
|
||||
python_parser_cf/ # Корень проекта
|
||||
├── python_parser/ # Пакет FastAPI + парсеры
|
||||
│ ├── app/ # FastAPI приложение
|
||||
│ │ ├── main.py # Основной файл приложения
|
||||
│ │ └── schemas/ # Pydantic схемы
|
||||
│ ├── core/ # Бизнес-логика
|
||||
│ │ ├── models.py # Модели данных
|
||||
│ │ ├── ports.py # Интерфейсы (порты)
|
||||
│ │ └── services.py # Сервисы
|
||||
│ ├── adapters/ # Адаптеры для внешних систем
|
||||
│ │ ├── storage.py # MinIO адаптер
|
||||
│ │ └── parsers/ # Парсеры Excel файлов
|
||||
│ ├── data/ # Тестовые данные
|
||||
│ ├── Dockerfile # Docker образ для FastAPI
|
||||
│ ├── requirements.txt # Зависимости FastAPI
|
||||
│ └── run_dev.py # Запуск FastAPI локально
|
||||
├── streamlit_app/ # Пакет Streamlit
|
||||
│ ├── app.py # Основное Streamlit приложение
|
||||
│ ├── requirements.txt # Зависимости Streamlit
|
||||
│ ├── Dockerfile # Docker образ для Streamlit
|
||||
│ ├── .streamlit/ # Конфигурация Streamlit
|
||||
│ │ └── config.toml # Настройки
|
||||
│ └── README.md # Документация Streamlit
|
||||
├── docker-compose.yml # Docker Compose конфигурация
|
||||
├── .gitignore # Git исключения
|
||||
└── README.md # Общая документация
|
||||
python_parser_cf/
|
||||
├── python_parser/ # FastAPI приложение
|
||||
│ ├── app/ # Основной код приложения
|
||||
│ ├── adapters/ # Адаптеры для парсеров
|
||||
│ ├── core/ # Основная бизнес-логика
|
||||
│ ├── data/ # Тестовые данные
|
||||
│ └── Dockerfile # Docker образ для FastAPI
|
||||
├── streamlit_app/ # Streamlit приложение
|
||||
│ ├── streamlit_app.py # Основной файл приложения
|
||||
│ ├── requirements.txt # Зависимости Python
|
||||
│ ├── .streamlit/ # Конфигурация Streamlit
|
||||
│ └── Dockerfile # Docker образ для Streamlit
|
||||
├── minio_data/ # Данные для MinIO
|
||||
├── docker-compose.yml # Конфигурация всех сервисов
|
||||
└── README.md # Документация
|
||||
```
|
||||
|
||||
## 🔍 Доступные эндпоинты
|
||||
## 🔧 Конфигурация
|
||||
|
||||
- **GET /** - Информация об API
|
||||
- **GET /docs** - Swagger документация
|
||||
- **GET /parsers** - Список доступных парсеров
|
||||
- **GET /parsers/{parser_name}/getters** - Информация о геттерах парсера
|
||||
- **POST /svodka_pm/upload-zip** - Загрузка сводок ПМ
|
||||
- **POST /svodka_ca/upload** - Загрузка сводок ЦА
|
||||
- **POST /monitoring_fuel/upload-zip** - Загрузка мониторинга топлива
|
||||
- **POST /svodka_pm/get_data** - Получение данных сводок ПМ
|
||||
- **POST /svodka_ca/get_data** - Получение данных сводок ЦА
|
||||
- **POST /monitoring_fuel/get_data** - Получение данных мониторинга топлива
|
||||
### Переменные окружения
|
||||
Все сервисы используют следующие переменные окружения:
|
||||
- `MINIO_ENDPOINT` - адрес MinIO сервера
|
||||
- `MINIO_ACCESS_KEY` - ключ доступа к MinIO
|
||||
- `MINIO_SECRET_KEY` - секретный ключ MinIO
|
||||
- `MINIO_SECURE` - использование SSL/TLS
|
||||
- `MINIO_BUCKET` - имя bucket'а для данных
|
||||
|
||||
## 📊 Поддерживаемые типы отчетов
|
||||
### Порты
|
||||
- **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
|
||||
# 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
|
||||
# Только MinIO
|
||||
docker-compose up -d minio
|
||||
|
||||
# MinIO + FastAPI
|
||||
docker-compose up -d minio fastapi
|
||||
|
||||
# Все сервисы
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 🛑 Остановка
|
||||
|
||||
### Остановка Docker сервисов:
|
||||
```bash
|
||||
# Все сервисы
|
||||
docker-compose down
|
||||
|
||||
# Только MinIO
|
||||
docker-compose stop minio
|
||||
```
|
||||
|
||||
### Остановка локальных сервисов:
|
||||
```bash
|
||||
# Нажмите Ctrl+C в терминале с FastAPI/Streamlit
|
||||
```
|
||||
|
||||
## 🔧 Разработка
|
||||
|
||||
### Добавление нового парсера:
|
||||
|
||||
1. Создайте файл в `python_parser/adapters/parsers/`
|
||||
2. Реализуйте интерфейс `ParserPort`
|
||||
3. Добавьте в `python_parser/core/services.py`
|
||||
4. Создайте схемы в `python_parser/app/schemas/`
|
||||
5. Добавьте эндпоинты в `python_parser/app/main.py`
|
||||
|
||||
### Тестирование:
|
||||
|
||||
```bash
|
||||
# Запуск тестов
|
||||
cd python_parser
|
||||
pytest
|
||||
pip install -r requirements.txt
|
||||
uvicorn app.main:app --reload
|
||||
```
|
||||
|
||||
# Запуск с покрытием
|
||||
pytest --cov=.
|
||||
### Локальная разработка Streamlit
|
||||
```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:
|
||||
print("\n🔧 РЕШЕНИЕ: Запустите FastAPI сервер")
|
||||
print("docker-compose up -d fastapi")
|
||||
print("python run_dev.py")
|
||||
|
||||
if not port_8501_ok:
|
||||
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__":
|
||||
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:
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
@@ -35,7 +37,13 @@ services:
|
||||
- "8501:8501"
|
||||
environment:
|
||||
- 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:
|
||||
- minio
|
||||
- fastapi
|
||||
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 в корне проекта.
|
||||
135
python_parser/SCHEMA_INTEGRATION.md
Normal file
135
python_parser/SCHEMA_INTEGRATION.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Интеграция схем Pydantic с парсерами
|
||||
|
||||
## Обзор
|
||||
|
||||
Этот документ описывает решение для устранения дублирования логики между схемами Pydantic и парсерами. Теперь схемы Pydantic являются единым источником правды для определения параметров парсеров.
|
||||
|
||||
## Проблема
|
||||
|
||||
Ранее в парсерах дублировалась информация о параметрах:
|
||||
|
||||
```python
|
||||
# В парсере
|
||||
self.register_getter(
|
||||
name="single_og",
|
||||
method=self._get_single_og,
|
||||
required_params=["id", "codes", "columns"], # Дублирование
|
||||
optional_params=["search"], # Дублирование
|
||||
description="Получение данных по одному ОГ"
|
||||
)
|
||||
|
||||
# В схеме
|
||||
class SvodkaPMSingleOGRequest(BaseModel):
|
||||
id: OGID = Field(...) # Обязательное поле
|
||||
codes: List[int] = Field(...) # Обязательное поле
|
||||
columns: List[str] = Field(...) # Обязательное поле
|
||||
search: Optional[str] = Field(None) # Необязательное поле
|
||||
```
|
||||
|
||||
## Решение
|
||||
|
||||
### 1. Утилиты для работы со схемами
|
||||
|
||||
Создан модуль `core/schema_utils.py` с функциями:
|
||||
|
||||
- `get_required_fields_from_schema()` - извлекает обязательные поля
|
||||
- `get_optional_fields_from_schema()` - извлекает необязательные поля
|
||||
- `register_getter_from_schema()` - регистрирует геттер с использованием схемы
|
||||
- `validate_params_with_schema()` - валидирует параметры с помощью схемы
|
||||
|
||||
### 2. Обновленные парсеры
|
||||
|
||||
Теперь парсеры используют схемы как единый источник правды:
|
||||
|
||||
```python
|
||||
def _register_default_getters(self):
|
||||
"""Регистрация геттеров по умолчанию"""
|
||||
# Используем схемы Pydantic как единый источник правды
|
||||
register_getter_from_schema(
|
||||
parser_instance=self,
|
||||
getter_name="single_og",
|
||||
method=self._get_single_og,
|
||||
schema_class=SvodkaPMSingleOGRequest,
|
||||
description="Получение данных по одному ОГ"
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Валидация параметров
|
||||
|
||||
Методы геттеров теперь автоматически валидируют параметры:
|
||||
|
||||
```python
|
||||
def _get_single_og(self, params: dict):
|
||||
"""Получение данных по одному ОГ"""
|
||||
# Валидируем параметры с помощью схемы Pydantic
|
||||
validated_params = validate_params_with_schema(params, SvodkaPMSingleOGRequest)
|
||||
|
||||
og_id = validated_params["id"]
|
||||
codes = validated_params["codes"]
|
||||
columns = validated_params["columns"]
|
||||
search = validated_params.get("search")
|
||||
|
||||
# ... остальная логика
|
||||
```
|
||||
|
||||
## Преимущества
|
||||
|
||||
1. **Единый источник правды** - информация о параметрах хранится только в схемах Pydantic
|
||||
2. **Автоматическая валидация** - параметры автоматически валидируются с помощью Pydantic
|
||||
3. **Синхронизация** - изменения в схемах автоматически отражаются в парсерах
|
||||
4. **Типобезопасность** - использование типов Pydantic обеспечивает типобезопасность
|
||||
5. **Документация** - Swagger документация автоматически генерируется из схем
|
||||
|
||||
## Совместимость
|
||||
|
||||
Решение работает с:
|
||||
- Pydantic v1 (через `__fields__`)
|
||||
- Pydantic v2 (через `model_fields` и `is_required()`)
|
||||
|
||||
## Использование
|
||||
|
||||
### Для новых парсеров
|
||||
|
||||
1. Создайте схему Pydantic с нужными полями
|
||||
2. Используйте `register_getter_from_schema()` для регистрации геттера
|
||||
3. Используйте `validate_params_with_schema()` в методах геттеров
|
||||
|
||||
### Для существующих парсеров
|
||||
|
||||
1. Убедитесь, что у вас есть соответствующая схема Pydantic
|
||||
2. Замените ручную регистрацию геттеров на `register_getter_from_schema()`
|
||||
3. Добавьте валидацию параметров в методы геттеров
|
||||
|
||||
## Примеры
|
||||
|
||||
### Схема с обязательными и необязательными полями
|
||||
|
||||
```python
|
||||
class ExampleRequest(BaseModel):
|
||||
required_field: str = Field(..., description="Обязательное поле")
|
||||
optional_field: Optional[str] = Field(None, description="Необязательное поле")
|
||||
```
|
||||
|
||||
### Регистрация геттера
|
||||
|
||||
```python
|
||||
register_getter_from_schema(
|
||||
parser_instance=self,
|
||||
getter_name="example_getter",
|
||||
method=self._example_method,
|
||||
schema_class=ExampleRequest,
|
||||
description="Пример геттера"
|
||||
)
|
||||
```
|
||||
|
||||
### Валидация в методе
|
||||
|
||||
```python
|
||||
def _example_method(self, params: dict):
|
||||
validated_params = validate_params_with_schema(params, ExampleRequest)
|
||||
# validated_params содержит валидированные данные
|
||||
```
|
||||
|
||||
## Заключение
|
||||
|
||||
Это решение устраняет дублирование кода и обеспечивает единообразие между API схемами и парсерами. Теперь изменения в схемах автоматически отражаются в парсерах, что упрощает поддержку и развитие системы.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
154
python_parser/adapters/local_storage.py
Normal file
154
python_parser/adapters/local_storage.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""
|
||||
Локальный storage адаптер для тестирования
|
||||
Сохраняет данные в локальную файловую систему вместо MinIO
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
import pandas as pd
|
||||
|
||||
from core.ports import StoragePort
|
||||
|
||||
|
||||
class LocalStorageAdapter(StoragePort):
|
||||
"""Локальный адаптер для хранения данных в файловой системе"""
|
||||
|
||||
def __init__(self, base_path: str = "local_storage"):
|
||||
"""
|
||||
Инициализация локального storage
|
||||
|
||||
Args:
|
||||
base_path: Базовый путь для хранения данных
|
||||
"""
|
||||
self.base_path = Path(base_path)
|
||||
self.base_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Создаем поддиректории
|
||||
(self.base_path / "data").mkdir(exist_ok=True)
|
||||
(self.base_path / "metadata").mkdir(exist_ok=True)
|
||||
|
||||
def object_exists(self, object_id: str) -> bool:
|
||||
"""Проверяет существование объекта"""
|
||||
data_file = self.base_path / "data" / f"{object_id}.pkl"
|
||||
return data_file.exists()
|
||||
|
||||
def save_dataframe(self, object_id: str, df: pd.DataFrame) -> bool:
|
||||
"""Сохраняет DataFrame в локальную файловую систему"""
|
||||
try:
|
||||
data_file = self.base_path / "data" / f"{object_id}.pkl"
|
||||
metadata_file = self.base_path / "metadata" / f"{object_id}.json"
|
||||
|
||||
# Сохраняем DataFrame
|
||||
with open(data_file, 'wb') as f:
|
||||
pickle.dump(df, f)
|
||||
|
||||
# Сохраняем метаданные
|
||||
metadata = {
|
||||
"object_id": object_id,
|
||||
"shape": df.shape,
|
||||
"columns": df.columns.tolist(),
|
||||
"dtypes": {str(k): str(v) for k, v in df.dtypes.to_dict().items()}
|
||||
}
|
||||
|
||||
with open(metadata_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(metadata, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при сохранении {object_id}: {e}")
|
||||
return False
|
||||
|
||||
def load_dataframe(self, object_id: str) -> Optional[pd.DataFrame]:
|
||||
"""Загружает DataFrame из локальной файловой системы"""
|
||||
try:
|
||||
data_file = self.base_path / "data" / f"{object_id}.pkl"
|
||||
|
||||
if not data_file.exists():
|
||||
return None
|
||||
|
||||
with open(data_file, 'rb') as f:
|
||||
df = pickle.load(f)
|
||||
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при загрузке {object_id}: {e}")
|
||||
return None
|
||||
|
||||
def delete_object(self, object_id: str) -> bool:
|
||||
"""Удаляет объект из локального storage"""
|
||||
try:
|
||||
data_file = self.base_path / "data" / f"{object_id}.pkl"
|
||||
metadata_file = self.base_path / "metadata" / f"{object_id}.json"
|
||||
|
||||
# Удаляем файлы если они существуют
|
||||
if data_file.exists():
|
||||
data_file.unlink()
|
||||
|
||||
if metadata_file.exists():
|
||||
metadata_file.unlink()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при удалении {object_id}: {e}")
|
||||
return False
|
||||
|
||||
def list_objects(self) -> list:
|
||||
"""Возвращает список всех объектов в storage"""
|
||||
try:
|
||||
data_dir = self.base_path / "data"
|
||||
if not data_dir.exists():
|
||||
return []
|
||||
|
||||
objects = []
|
||||
for file_path in data_dir.glob("*.pkl"):
|
||||
object_id = file_path.stem
|
||||
objects.append(object_id)
|
||||
|
||||
return objects
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при получении списка объектов: {e}")
|
||||
return []
|
||||
|
||||
def get_object_metadata(self, object_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Возвращает метаданные объекта"""
|
||||
try:
|
||||
metadata_file = self.base_path / "metadata" / f"{object_id}.json"
|
||||
|
||||
if not metadata_file.exists():
|
||||
return None
|
||||
|
||||
with open(metadata_file, 'r', encoding='utf-8') as f:
|
||||
metadata = json.load(f)
|
||||
|
||||
return metadata
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при получении метаданных {object_id}: {e}")
|
||||
return None
|
||||
|
||||
def clear_all(self) -> bool:
|
||||
"""Очищает весь storage"""
|
||||
try:
|
||||
data_dir = self.base_path / "data"
|
||||
metadata_dir = self.base_path / "metadata"
|
||||
|
||||
# Удаляем все файлы
|
||||
for file_path in data_dir.glob("*"):
|
||||
if file_path.is_file():
|
||||
file_path.unlink()
|
||||
|
||||
for file_path in metadata_dir.glob("*"):
|
||||
if file_path.is_file():
|
||||
file_path.unlink()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Ошибка при очистке storage: {e}")
|
||||
return False
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,9 @@ import re
|
||||
import zipfile
|
||||
from typing import Dict, Tuple
|
||||
from core.ports import ParserPort
|
||||
from adapters.pconfig import data_to_json
|
||||
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
||||
from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest, MonitoringFuelSeriesRequest
|
||||
from adapters.pconfig import data_to_json, find_header_row
|
||||
|
||||
|
||||
class MonitoringFuelParser(ParserPort):
|
||||
@@ -13,37 +15,48 @@ class MonitoringFuelParser(ParserPort):
|
||||
|
||||
def _register_default_getters(self):
|
||||
"""Регистрация геттеров по умолчанию"""
|
||||
self.register_getter(
|
||||
name="total_by_columns",
|
||||
# Используем схемы Pydantic как единый источник правды
|
||||
register_getter_from_schema(
|
||||
parser_instance=self,
|
||||
getter_name="total_by_columns",
|
||||
method=self._get_total_by_columns,
|
||||
required_params=["columns"],
|
||||
optional_params=[],
|
||||
schema_class=MonitoringFuelTotalRequest,
|
||||
description="Агрегация данных по колонкам"
|
||||
)
|
||||
|
||||
self.register_getter(
|
||||
name="month_by_code",
|
||||
register_getter_from_schema(
|
||||
parser_instance=self,
|
||||
getter_name="month_by_code",
|
||||
method=self._get_month_by_code,
|
||||
required_params=["month"],
|
||||
optional_params=[],
|
||||
schema_class=MonitoringFuelMonthRequest,
|
||||
description="Получение данных за конкретный месяц"
|
||||
)
|
||||
|
||||
register_getter_from_schema(
|
||||
parser_instance=self,
|
||||
getter_name="series_by_id_and_columns",
|
||||
method=self._get_series_by_id_and_columns,
|
||||
schema_class=MonitoringFuelSeriesRequest,
|
||||
description="Получение временных рядов по ID и колонкам"
|
||||
)
|
||||
|
||||
def _get_total_by_columns(self, params: dict):
|
||||
"""Агрегация по колонкам (обертка для совместимости)"""
|
||||
columns = params["columns"]
|
||||
if not columns:
|
||||
raise ValueError("Отсутствуют идентификаторы столбцов")
|
||||
"""Агрегация данных по колонкам"""
|
||||
# Валидируем параметры с помощью схемы Pydantic
|
||||
validated_params = validate_params_with_schema(params, MonitoringFuelTotalRequest)
|
||||
|
||||
columns = validated_params["columns"]
|
||||
|
||||
# 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):
|
||||
"""Получение данных за месяц (обертка для совместимости)"""
|
||||
month = params["month"]
|
||||
if not month:
|
||||
raise ValueError("Отсутствует идентификатор месяца")
|
||||
"""Получение данных за конкретный месяц"""
|
||||
# Валидируем параметры с помощью схемы Pydantic
|
||||
validated_params = validate_params_with_schema(params, MonitoringFuelMonthRequest)
|
||||
|
||||
month = validated_params["month"]
|
||||
|
||||
# TODO: Переделать под новую архитектуру
|
||||
df_month = self.get_month(self.df, month)
|
||||
@@ -87,30 +100,13 @@ class MonitoringFuelParser(ParserPort):
|
||||
|
||||
return df_monitorings
|
||||
|
||||
def find_header_row(self, file_path: str, sheet: str, search_value: str = "Установка", max_rows: int = 50) -> int:
|
||||
"""Определение индекса заголовка в 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="Установка")
|
||||
header_num = find_header_row(file, sheet, search_value="Установка")
|
||||
# Читаем весь лист, начиная с найденной строки как заголовок
|
||||
df_full = pd.read_excel(
|
||||
file,
|
||||
@@ -232,3 +228,47 @@ class MonitoringFuelParser(ParserPort):
|
||||
total.name = 'mean'
|
||||
|
||||
return total, df_combined
|
||||
|
||||
def _get_series_by_id_and_columns(self, params: dict):
|
||||
"""Получение временных рядов по ID и колонкам"""
|
||||
# Валидируем параметры с помощью схемы Pydantic
|
||||
validated_params = validate_params_with_schema(params, MonitoringFuelSeriesRequest)
|
||||
|
||||
columns = validated_params["columns"]
|
||||
|
||||
# Проверяем, что все колонки существуют хотя бы в одном месяце
|
||||
valid_columns = set()
|
||||
for month in self.df.values():
|
||||
valid_columns.update(month.columns)
|
||||
|
||||
for col in columns:
|
||||
if col not in valid_columns:
|
||||
raise ValueError(f"Колонка '{col}' не найдена ни в одном месяце")
|
||||
|
||||
# Подготавливаем результат: словарь id → {col: [значения по месяцам]}
|
||||
result = {}
|
||||
|
||||
# Обрабатываем месяцы от 01 до 12
|
||||
for month_key in [f"{i:02d}" for i in range(1, 13)]:
|
||||
if month_key not in self.df:
|
||||
print(f"Месяц '{month_key}' не найден в df_monitorings, пропускаем.")
|
||||
continue
|
||||
|
||||
df = self.df[month_key]
|
||||
|
||||
for col in columns:
|
||||
if col not in df.columns:
|
||||
continue # Пропускаем, если в этом месяце нет колонки
|
||||
|
||||
for idx, value in df[col].items():
|
||||
if pd.isna(value):
|
||||
continue # Можно пропустить NaN, или оставить как null
|
||||
|
||||
if idx not in result:
|
||||
result[idx] = {c: [] for c in columns}
|
||||
|
||||
result[idx][col].append(value)
|
||||
|
||||
# Преобразуем ключи id в строки (для JSON-совместимости)
|
||||
result_str_keys = {str(k): v for k, v in result.items()}
|
||||
return result_str_keys
|
||||
|
||||
@@ -2,6 +2,8 @@ import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
from core.ports import ParserPort
|
||||
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
||||
from app.schemas.svodka_ca import SvodkaCARequest
|
||||
from adapters.pconfig import get_og_by_name
|
||||
|
||||
|
||||
@@ -12,23 +14,22 @@ class SvodkaCAParser(ParserPort):
|
||||
|
||||
def _register_default_getters(self):
|
||||
"""Регистрация геттеров по умолчанию"""
|
||||
self.register_getter(
|
||||
name="get_data",
|
||||
# Используем схемы Pydantic как единый источник правды
|
||||
register_getter_from_schema(
|
||||
parser_instance=self,
|
||||
getter_name="get_ca_data",
|
||||
method=self._get_data_wrapper,
|
||||
required_params=["modes", "tables"],
|
||||
optional_params=[],
|
||||
schema_class=SvodkaCARequest,
|
||||
description="Получение данных по режимам и таблицам"
|
||||
)
|
||||
|
||||
def _get_data_wrapper(self, params: dict):
|
||||
"""Обертка для получения данных (для совместимости)"""
|
||||
modes = params["modes"]
|
||||
tables = params["tables"]
|
||||
"""Получение данных по режимам и таблицам"""
|
||||
# Валидируем параметры с помощью схемы Pydantic
|
||||
validated_params = validate_params_with_schema(params, SvodkaCARequest)
|
||||
|
||||
if not isinstance(modes, list):
|
||||
raise ValueError("Поле 'modes' должно быть списком")
|
||||
if not isinstance(tables, list):
|
||||
raise ValueError("Поле 'tables' должно быть списком")
|
||||
modes = validated_params["modes"]
|
||||
tables = validated_params["tables"]
|
||||
|
||||
# TODO: Переделать под новую архитектуру
|
||||
data_dict = {}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import pandas as pd
|
||||
|
||||
from core.ports import ParserPort
|
||||
from adapters.pconfig import OG_IDS, replace_id_in_path, data_to_json
|
||||
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
||||
from app.schemas.svodka_pm import SvodkaPMSingleOGRequest, SvodkaPMTotalOGsRequest
|
||||
from adapters.pconfig import SINGLE_OGS, replace_id_in_path, data_to_json, find_header_row
|
||||
|
||||
|
||||
class SvodkaPMParser(ParserPort):
|
||||
@@ -11,48 +13,45 @@ class SvodkaPMParser(ParserPort):
|
||||
|
||||
def _register_default_getters(self):
|
||||
"""Регистрация геттеров по умолчанию"""
|
||||
self.register_getter(
|
||||
name="single_og",
|
||||
# Используем схемы Pydantic как единый источник правды
|
||||
register_getter_from_schema(
|
||||
parser_instance=self,
|
||||
getter_name="single_og",
|
||||
method=self._get_single_og,
|
||||
required_params=["id", "codes", "columns"],
|
||||
optional_params=["search"],
|
||||
schema_class=SvodkaPMSingleOGRequest,
|
||||
description="Получение данных по одному ОГ"
|
||||
)
|
||||
|
||||
self.register_getter(
|
||||
name="total_ogs",
|
||||
register_getter_from_schema(
|
||||
parser_instance=self,
|
||||
getter_name="total_ogs",
|
||||
method=self._get_total_ogs,
|
||||
required_params=["codes", "columns"],
|
||||
optional_params=["search"],
|
||||
schema_class=SvodkaPMTotalOGsRequest,
|
||||
description="Получение данных по всем ОГ"
|
||||
)
|
||||
|
||||
def _get_single_og(self, params: dict):
|
||||
"""Получение данных по одному ОГ (обертка для совместимости)"""
|
||||
og_id = params["id"]
|
||||
codes = params["codes"]
|
||||
columns = params["columns"]
|
||||
search = params.get("search")
|
||||
"""Получение данных по одному ОГ"""
|
||||
# Валидируем параметры с помощью схемы Pydantic
|
||||
validated_params = validate_params_with_schema(params, SvodkaPMSingleOGRequest)
|
||||
|
||||
if not isinstance(codes, list):
|
||||
raise ValueError("Поле 'codes' должно быть списком")
|
||||
if not isinstance(columns, list):
|
||||
raise ValueError("Поле 'columns' должно быть списком")
|
||||
og_id = validated_params["id"]
|
||||
codes = validated_params["codes"]
|
||||
columns = validated_params["columns"]
|
||||
search = validated_params.get("search")
|
||||
|
||||
# Здесь нужно получить 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")
|
||||
"""Получение данных по всем ОГ"""
|
||||
# Валидируем параметры с помощью схемы Pydantic
|
||||
validated_params = validate_params_with_schema(params, SvodkaPMTotalOGsRequest)
|
||||
|
||||
if not isinstance(codes, list):
|
||||
raise ValueError("Поле 'codes' должно быть списком")
|
||||
if not isinstance(columns, list):
|
||||
raise ValueError("Поле 'columns' должно быть списком")
|
||||
codes = validated_params["codes"]
|
||||
columns = validated_params["columns"]
|
||||
search = validated_params.get("search")
|
||||
|
||||
# TODO: Переделать под новую архитектуру
|
||||
return self.get_svodka_total(self.df, codes, columns, search)
|
||||
@@ -63,30 +62,13 @@ class SvodkaPMParser(ParserPort):
|
||||
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:
|
||||
"""Определения индекса заголовка в excel по ключевому слову"""
|
||||
# Читаем первые max_rows строк без заголовков
|
||||
df_temp = pd.read_excel(
|
||||
file,
|
||||
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 # 0-based index — то, что нужно для header=
|
||||
|
||||
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
||||
|
||||
def parse_svodka_pm(self, file, sheet, header_num=None):
|
||||
''' Собственно парсер отчетов одного ОГ для БП, ПП и факта '''
|
||||
# Автоопределение header_num, если не передан
|
||||
if header_num is None:
|
||||
header_num = self.find_header_row(file, sheet, search_value="Итого")
|
||||
header_num = find_header_row(file, sheet, search_value="Итого")
|
||||
|
||||
# Читаем заголовки header_num и 1-2 строки данных, чтобы найти INDICATOR_ID
|
||||
df_probe = pd.read_excel(
|
||||
@@ -184,7 +166,7 @@ class SvodkaPMParser(ParserPort):
|
||||
excel_plan_template = 'svodka_plan_pm_ID.xlsx'
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
file_list = zip_ref.namelist()
|
||||
for name, id in OG_IDS.items():
|
||||
for id in SINGLE_OGS:
|
||||
if id == 'BASH':
|
||||
continue # пропускаем BASH
|
||||
|
||||
@@ -291,11 +273,11 @@ class SvodkaPMParser(ParserPort):
|
||||
''' Служебная функция агрегации данные по всем ОГ '''
|
||||
total_result = {}
|
||||
|
||||
for name, og_id in OG_IDS.items():
|
||||
for og_id in SINGLE_OGS:
|
||||
if og_id == 'BASH':
|
||||
continue
|
||||
|
||||
# print(f"📊 Обработка: {name} ({og_id})")
|
||||
# print(f"📊 Обработка: {og_id}")
|
||||
try:
|
||||
data = self.get_svodka_og(
|
||||
pm_dict,
|
||||
@@ -306,7 +288,7 @@ class SvodkaPMParser(ParserPort):
|
||||
)
|
||||
total_result[og_id] = data
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка при обработке {name} ({og_id}): {e}")
|
||||
print(f"❌ Ошибка при обработке {og_id}: {e}")
|
||||
total_result[og_id] = None
|
||||
|
||||
return total_result
|
||||
|
||||
@@ -3,6 +3,7 @@ from functools import lru_cache
|
||||
import json
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import os
|
||||
|
||||
OG_IDS = {
|
||||
"Комсомольский НПЗ": "KNPZ",
|
||||
@@ -22,8 +23,37 @@ OG_IDS = {
|
||||
"Красноленинский НПЗ": "KLNPZ",
|
||||
"Пурнефтепереработка": "PurNP",
|
||||
"ЯНОС": "YANOS",
|
||||
"Уфанефтехим": "UNH",
|
||||
"РНПК": "RNPK",
|
||||
"КмсНПЗ": "KNPZ",
|
||||
"АНХК": "ANHK",
|
||||
"НК НПЗ": "NovKuybNPZ",
|
||||
"КНПЗ": "KuybNPZ",
|
||||
"СНПЗ": "CyzNPZ",
|
||||
"Нижневаторское НПО": "NVNPO",
|
||||
"ПурНП": "PurNP",
|
||||
}
|
||||
|
||||
SINGLE_OGS = [
|
||||
"KNPZ",
|
||||
"ANHK",
|
||||
"AchNPZ",
|
||||
"BASH",
|
||||
"UNPZ",
|
||||
"UNH",
|
||||
"NOV",
|
||||
"NovKuybNPZ",
|
||||
"KuybNPZ",
|
||||
"CyzNPZ",
|
||||
"TuapsNPZ",
|
||||
"SNPZ",
|
||||
"RNPK",
|
||||
"NVNPO",
|
||||
"KLNPZ",
|
||||
"PurNP",
|
||||
"YANOS",
|
||||
]
|
||||
|
||||
SNPZ_IDS = {
|
||||
"Висбрекинг": "SNPZ.VISB",
|
||||
"Изомеризация": "SNPZ.IZOM",
|
||||
@@ -40,7 +70,18 @@ SNPZ_IDS = {
|
||||
|
||||
|
||||
def replace_id_in_path(file_path, new_id):
|
||||
return file_path.replace('ID', str(new_id))
|
||||
# Заменяем 'ID' на новое значение
|
||||
modified_path = file_path.replace('ID', str(new_id)) + '.xlsx'
|
||||
|
||||
# Проверяем, существует ли файл
|
||||
if not os.path.exists(modified_path):
|
||||
# Меняем расширение на .xlsm
|
||||
directory, filename = os.path.split(modified_path)
|
||||
name, ext = os.path.splitext(filename)
|
||||
new_filename = name + '.xlsm'
|
||||
modified_path = os.path.join(directory, new_filename)
|
||||
|
||||
return modified_path
|
||||
|
||||
|
||||
def get_table_name(exel):
|
||||
@@ -109,6 +150,25 @@ def get_id_by_name(name, dictionary):
|
||||
return best_match
|
||||
|
||||
|
||||
def find_header_row(file, sheet, search_value="Итого", max_rows=50):
|
||||
''' Определения индекса заголовка в exel по ключевому слову '''
|
||||
# Читаем первые max_rows строк без заголовков
|
||||
df_temp = pd.read_excel(
|
||||
file,
|
||||
sheet_name=sheet,
|
||||
header=None,
|
||||
nrows=max_rows
|
||||
)
|
||||
|
||||
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
||||
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 # 0-based index — то, что нужно для header=
|
||||
|
||||
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
||||
|
||||
|
||||
def data_to_json(data, indent=2, ensure_ascii=False):
|
||||
"""
|
||||
Полностью безопасная сериализация данных в JSON.
|
||||
@@ -175,7 +235,6 @@ def data_to_json(data, indent=2, ensure_ascii=False):
|
||||
|
||||
try:
|
||||
cleaned_data = convert_obj(data)
|
||||
cleaned_data_str = json.dumps(cleaned_data, indent=indent, ensure_ascii=ensure_ascii)
|
||||
return cleaned_data
|
||||
return json.dumps(cleaned_data, indent=indent, ensure_ascii=ensure_ascii)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Не удалось сериализовать данные в JSON: {e}")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -16,7 +16,7 @@ from app.schemas import (
|
||||
UploadResponse, UploadErrorResponse,
|
||||
SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest,
|
||||
SvodkaCARequest,
|
||||
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest
|
||||
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest
|
||||
)
|
||||
|
||||
|
||||
@@ -377,7 +377,7 @@ async def get_svodka_pm_total_ogs(
|
||||
try:
|
||||
# Создаем запрос
|
||||
request_dict = request_data.model_dump()
|
||||
request_dict['mode'] = 'total'
|
||||
request_dict['mode'] = 'total_ogs'
|
||||
request = DataRequest(
|
||||
report_type='svodka_pm',
|
||||
get_params=request_dict
|
||||
@@ -400,41 +400,6 @@ async def get_svodka_pm_total_ogs(
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/svodka_pm/get_data", tags=[SvodkaPMParser.name])
|
||||
async def get_svodka_pm_data(
|
||||
request_data: dict
|
||||
):
|
||||
report_service = get_report_service()
|
||||
"""
|
||||
Получение данных из отчета сводки факта СарНПЗ
|
||||
|
||||
- indicator_id: ID индикатора
|
||||
- code: Код для поиска
|
||||
- search_value: Опциональное значение для поиска
|
||||
"""
|
||||
try:
|
||||
# Создаем запрос
|
||||
request = DataRequest(
|
||||
report_type='svodka_pm',
|
||||
get_params=request_data
|
||||
)
|
||||
|
||||
# Получаем данные
|
||||
result = report_service.get_data(request)
|
||||
|
||||
if result.success:
|
||||
return {
|
||||
"success": True,
|
||||
"data": result.data
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=result.message)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/svodka_ca/upload", tags=[SvodkaCAParser.name],
|
||||
summary="Загрузка файла отчета сводки СА",
|
||||
@@ -509,7 +474,7 @@ async def upload_svodka_ca(
|
||||
)
|
||||
|
||||
|
||||
@app.post("/svodka_ca/get_data", tags=[SvodkaCAParser.name],
|
||||
@app.post("/svodka_ca/get_ca_data", tags=[SvodkaCAParser.name],
|
||||
summary="Получение данных из отчета сводки СА")
|
||||
async def get_svodka_ca_data(
|
||||
request_data: SvodkaCARequest
|
||||
@@ -534,6 +499,7 @@ async def get_svodka_ca_data(
|
||||
try:
|
||||
# Создаем запрос
|
||||
request_dict = request_data.model_dump()
|
||||
request_dict['mode'] = 'get_ca_data'
|
||||
request = DataRequest(
|
||||
report_type='svodka_ca',
|
||||
get_params=request_dict
|
||||
@@ -610,38 +576,6 @@ async def get_svodka_ca_data(
|
||||
# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/monitoring_fuel/get_data", tags=[MonitoringFuelParser.name])
|
||||
async def get_monitoring_fuel_data(
|
||||
request_data: dict
|
||||
):
|
||||
report_service = get_report_service()
|
||||
"""
|
||||
Получение данных из отчета мониторинга топлива
|
||||
|
||||
- column: Название колонки для агрегации (normativ, total, total_svod)
|
||||
"""
|
||||
try:
|
||||
# Создаем запрос
|
||||
request = DataRequest(
|
||||
report_type='monitoring_fuel',
|
||||
get_params=request_data
|
||||
)
|
||||
|
||||
# Получаем данные
|
||||
result = report_service.get_data(request)
|
||||
|
||||
if result.success:
|
||||
return {
|
||||
"success": True,
|
||||
"data": result.data
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=result.message)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
|
||||
# @app.post("/monitoring_fuel/upload_directory", tags=[MonitoringFuelParser.name])
|
||||
@@ -804,7 +738,7 @@ async def get_monitoring_fuel_total_by_columns(
|
||||
try:
|
||||
# Создаем запрос
|
||||
request_dict = request_data.model_dump()
|
||||
request_dict['mode'] = 'total'
|
||||
request_dict['mode'] = 'total_by_columns'
|
||||
request = DataRequest(
|
||||
report_type='monitoring_fuel',
|
||||
get_params=request_dict
|
||||
@@ -849,7 +783,56 @@ async def get_monitoring_fuel_month_by_code(
|
||||
try:
|
||||
# Создаем запрос
|
||||
request_dict = request_data.model_dump()
|
||||
request_dict['mode'] = 'month'
|
||||
request_dict['mode'] = 'month_by_code'
|
||||
request = DataRequest(
|
||||
report_type='monitoring_fuel',
|
||||
get_params=request_dict
|
||||
)
|
||||
|
||||
# Получаем данные
|
||||
result = report_service.get_data(request)
|
||||
|
||||
if result.success:
|
||||
return {
|
||||
"success": True,
|
||||
"data": result.data
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail=result.message)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/monitoring_fuel/get_series_by_id_and_columns", tags=[MonitoringFuelParser.name],
|
||||
summary="Получение временных рядов по ID и колонкам")
|
||||
async def get_monitoring_fuel_series_by_id_and_columns(
|
||||
request_data: MonitoringFuelSeriesRequest
|
||||
):
|
||||
"""Получение временных рядов из сводок мониторинга топлива по ID и колонкам
|
||||
|
||||
### Структура параметров:
|
||||
- `columns`: **Массив названий** выбираемых столбцов для получения временных рядов (обязательный)
|
||||
|
||||
### Пример тела запроса:
|
||||
```json
|
||||
{
|
||||
"columns": ["total", "normativ"]
|
||||
}
|
||||
```
|
||||
|
||||
### Возвращает:
|
||||
Словарь где ключ - ID объекта, значение - словарь с колонками,
|
||||
в которых хранятся списки значений по месяцам.
|
||||
"""
|
||||
report_service = get_report_service()
|
||||
|
||||
try:
|
||||
# Создаем запрос
|
||||
request_dict = request_data.model_dump()
|
||||
request_dict['mode'] = 'series_by_id_and_columns'
|
||||
request = DataRequest(
|
||||
report_type='monitoring_fuel',
|
||||
get_params=request_dict
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .monitoring_fuel import MonitoringFuelMonthRequest, MonitoringFuelTotalRequest
|
||||
from .monitoring_fuel import MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest
|
||||
from .svodka_ca import SvodkaCARequest
|
||||
from .svodka_pm import SvodkaPMSingleOGRequest, SvodkaPMTotalOGsRequest
|
||||
from .server import ServerInfoResponse
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -32,3 +32,19 @@ class MonitoringFuelTotalRequest(BaseModel):
|
||||
"columns": ["total", "normativ"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MonitoringFuelSeriesRequest(BaseModel):
|
||||
columns: List[str] = Field(
|
||||
...,
|
||||
description="Массив названий выбираемых столбцов для получения временных рядов",
|
||||
example=["total", "normativ"],
|
||||
min_items=1
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"columns": ["total", "normativ"]
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
144
python_parser/core/schema_utils.py
Normal file
144
python_parser/core/schema_utils.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Упрощенные утилиты для работы со схемами Pydantic
|
||||
"""
|
||||
from typing import List, Dict, Any, Type
|
||||
from pydantic import BaseModel
|
||||
import inspect
|
||||
|
||||
|
||||
def get_required_fields_from_schema(schema_class: Type[BaseModel]) -> List[str]:
|
||||
"""
|
||||
Извлекает список обязательных полей из схемы Pydantic
|
||||
|
||||
Args:
|
||||
schema_class: Класс схемы Pydantic
|
||||
|
||||
Returns:
|
||||
Список имен обязательных полей
|
||||
"""
|
||||
required_fields = []
|
||||
|
||||
# Используем model_fields для Pydantic v2 или __fields__ для v1
|
||||
if hasattr(schema_class, 'model_fields'):
|
||||
fields = schema_class.model_fields
|
||||
else:
|
||||
fields = schema_class.__fields__
|
||||
|
||||
for field_name, field_info in fields.items():
|
||||
# В Pydantic v2 есть метод is_required()
|
||||
if hasattr(field_info, 'is_required'):
|
||||
if field_info.is_required():
|
||||
required_fields.append(field_name)
|
||||
elif hasattr(field_info, 'required'):
|
||||
if field_info.required:
|
||||
required_fields.append(field_name)
|
||||
else:
|
||||
# Fallback для старых версий - проверяем наличие default
|
||||
has_default = False
|
||||
|
||||
if hasattr(field_info, 'default'):
|
||||
has_default = field_info.default is not ...
|
||||
elif hasattr(field_info, 'default_factory'):
|
||||
has_default = field_info.default_factory is not None
|
||||
|
||||
if not has_default:
|
||||
required_fields.append(field_name)
|
||||
|
||||
return required_fields
|
||||
|
||||
|
||||
def get_optional_fields_from_schema(schema_class: Type[BaseModel]) -> List[str]:
|
||||
"""
|
||||
Извлекает список необязательных полей из схемы Pydantic
|
||||
|
||||
Args:
|
||||
schema_class: Класс схемы Pydantic
|
||||
|
||||
Returns:
|
||||
Список имен необязательных полей
|
||||
"""
|
||||
optional_fields = []
|
||||
|
||||
# Используем model_fields для Pydantic v2 или __fields__ для v1
|
||||
if hasattr(schema_class, 'model_fields'):
|
||||
fields = schema_class.model_fields
|
||||
else:
|
||||
fields = schema_class.__fields__
|
||||
|
||||
for field_name, field_info in fields.items():
|
||||
# В Pydantic v2 есть метод is_required()
|
||||
if hasattr(field_info, 'is_required'):
|
||||
if not field_info.is_required():
|
||||
optional_fields.append(field_name)
|
||||
elif hasattr(field_info, 'required'):
|
||||
if not field_info.required:
|
||||
optional_fields.append(field_name)
|
||||
else:
|
||||
# Fallback для старых версий - проверяем наличие default
|
||||
has_default = False
|
||||
|
||||
if hasattr(field_info, 'default'):
|
||||
has_default = field_info.default is not ...
|
||||
elif hasattr(field_info, 'default_factory'):
|
||||
has_default = field_info.default_factory is not None
|
||||
|
||||
if has_default:
|
||||
optional_fields.append(field_name)
|
||||
|
||||
return optional_fields
|
||||
|
||||
|
||||
def register_getter_from_schema(parser_instance, getter_name: str, method: callable,
|
||||
schema_class: Type[BaseModel], description: str = ""):
|
||||
"""
|
||||
Регистрирует геттер в парсере, используя схему Pydantic для определения параметров
|
||||
|
||||
Args:
|
||||
parser_instance: Экземпляр парсера
|
||||
getter_name: Имя геттера
|
||||
method: Метод для выполнения
|
||||
schema_class: Класс схемы Pydantic
|
||||
description: Описание геттера (если не указано, берется из docstring метода)
|
||||
"""
|
||||
# Извлекаем параметры из схемы
|
||||
required_params = get_required_fields_from_schema(schema_class)
|
||||
optional_params = get_optional_fields_from_schema(schema_class)
|
||||
|
||||
# Если описание не указано, берем из docstring метода
|
||||
if not description:
|
||||
description = inspect.getdoc(method) or ""
|
||||
|
||||
# Регистрируем геттер
|
||||
parser_instance.register_getter(
|
||||
name=getter_name,
|
||||
method=method,
|
||||
required_params=required_params,
|
||||
optional_params=optional_params,
|
||||
description=description
|
||||
)
|
||||
|
||||
|
||||
def validate_params_with_schema(params: Dict[str, Any], schema_class: Type[BaseModel]) -> Dict[str, Any]:
|
||||
"""
|
||||
Валидирует параметры с помощью схемы Pydantic
|
||||
|
||||
Args:
|
||||
params: Словарь параметров
|
||||
schema_class: Класс схемы Pydantic
|
||||
|
||||
Returns:
|
||||
Валидированные параметры
|
||||
|
||||
Raises:
|
||||
ValidationError: Если параметры не прошли валидацию
|
||||
"""
|
||||
try:
|
||||
# Создаем экземпляр схемы для валидации
|
||||
validated_data = schema_class(**params)
|
||||
# Используем model_dump() для Pydantic v2 или dict() для v1
|
||||
if hasattr(validated_data, 'model_dump'):
|
||||
return validated_data.model_dump()
|
||||
else:
|
||||
return validated_data.dict()
|
||||
except Exception as e:
|
||||
raise ValueError(f"Ошибка валидации параметров: {str(e)}")
|
||||
@@ -106,14 +106,14 @@ class ReportService:
|
||||
# Получаем параметры запроса
|
||||
get_params = request.get_params or {}
|
||||
|
||||
# Определяем имя геттера (по умолчанию используем первый доступный)
|
||||
getter_name = get_params.pop("getter", None)
|
||||
# Определяем имя геттера из параметра mode
|
||||
getter_name = get_params.pop("mode", None)
|
||||
if not getter_name:
|
||||
# Если геттер не указан, берем первый доступный
|
||||
# Если режим не указан, берем первый доступный
|
||||
available_getters = list(parser.getters.keys())
|
||||
if available_getters:
|
||||
getter_name = available_getters[0]
|
||||
print(f"⚠️ Геттер не указан, используем первый доступный: {getter_name}")
|
||||
print(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
||||
else:
|
||||
return DataResult(
|
||||
success=False,
|
||||
|
||||
@@ -11,5 +11,4 @@ requests>=2.31.0
|
||||
# pytest-cov>=4.0.0
|
||||
# pytest-mock>=3.10.0
|
||||
httpx>=0.24.0
|
||||
numpy
|
||||
streamlit>=1.28.0
|
||||
numpy
|
||||
@@ -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
|
||||
|
||||
# Устанавливаем системные зависимости
|
||||
# Установка системных зависимостей
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Копируем файлы зависимостей
|
||||
# Копирование requirements.txt
|
||||
COPY requirements.txt .
|
||||
|
||||
# Устанавливаем Python зависимости
|
||||
# Установка Python зависимостей
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем код приложения
|
||||
# Копирование кода приложения
|
||||
COPY . .
|
||||
|
||||
# Открываем порт
|
||||
# Открытие порта
|
||||
EXPOSE 8501
|
||||
|
||||
# Команда запуска
|
||||
CMD ["streamlit", "run", "app.py", "--server.port", "8501", "--server.address", "0.0.0.0"]
|
||||
# Запуск Streamlit
|
||||
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
|
||||
requests>=2.31.0
|
||||
pandas>=1.5.0
|
||||
numpy>=1.24.0
|
||||
pandas>=2.0.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"
|
||||
)
|
||||
|
||||
# Конфигурация API - автоматически определяем правильный адрес
|
||||
def get_api_base_url():
|
||||
"""Автоматически определяет правильный адрес API"""
|
||||
# Если запущено в Docker, используем внутренний адрес
|
||||
if os.getenv("DOCKER_ENV") == "true":
|
||||
return "http://fastapi:8000"
|
||||
|
||||
# Если запущено локально, используем localhost
|
||||
return "http://localhost:8000"
|
||||
|
||||
API_BASE_URL = os.getenv("API_BASE_URL", get_api_base_url())
|
||||
# Конфигурация API
|
||||
API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker
|
||||
API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя
|
||||
|
||||
def check_api_health():
|
||||
"""Проверка доступности API"""
|
||||
@@ -45,16 +37,6 @@ def get_available_parsers():
|
||||
except:
|
||||
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():
|
||||
"""Получение информации о сервере"""
|
||||
try:
|
||||
@@ -92,7 +74,7 @@ def main():
|
||||
st.info("Убедитесь, что FastAPI сервер запущен")
|
||||
return
|
||||
|
||||
st.success(f"✅ API доступен по адресу {API_BASE_URL}")
|
||||
st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}")
|
||||
|
||||
# Боковая панель с информацией
|
||||
with st.sidebar:
|
||||
@@ -124,9 +106,6 @@ def main():
|
||||
with tab1:
|
||||
st.header("📊 Сводки ПМ - Полный функционал")
|
||||
|
||||
# Получаем информацию о геттерах
|
||||
getters_info = get_parser_getters("svodka_pm")
|
||||
|
||||
# Секция загрузки файлов
|
||||
st.subheader("📤 Загрузка файлов")
|
||||
uploaded_pm = st.file_uploader(
|
||||
@@ -155,15 +134,6 @@ def main():
|
||||
# Секция получения данных
|
||||
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)
|
||||
|
||||
with col1:
|
||||
@@ -195,13 +165,12 @@ def main():
|
||||
if codes and columns:
|
||||
with st.spinner("Получаю данные..."):
|
||||
data = {
|
||||
"getter": "single_og",
|
||||
"id": og_id,
|
||||
"codes": codes,
|
||||
"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:
|
||||
st.success("✅ Данные получены")
|
||||
@@ -232,12 +201,11 @@ def main():
|
||||
if codes_total and columns_total:
|
||||
with st.spinner("Получаю данные..."):
|
||||
data = {
|
||||
"getter": "total_ogs",
|
||||
"codes": codes_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:
|
||||
st.success("✅ Данные получены")
|
||||
@@ -251,9 +219,6 @@ def main():
|
||||
with tab2:
|
||||
st.header("🏭 Сводки СА - Полный функционал")
|
||||
|
||||
# Получаем информацию о геттерах
|
||||
getters_info = get_parser_getters("svodka_ca")
|
||||
|
||||
# Секция загрузки файлов
|
||||
st.subheader("📤 Загрузка файлов")
|
||||
uploaded_ca = st.file_uploader(
|
||||
@@ -281,16 +246,7 @@ def main():
|
||||
st.markdown("---")
|
||||
|
||||
# Секция получения данных
|
||||
st.subheader("<EFBFBD><EFBFBD> Получение данных")
|
||||
|
||||
# Показываем доступные геттеры
|
||||
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'])}")
|
||||
st.subheader("🔍 Получение данных")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
@@ -317,18 +273,17 @@ def main():
|
||||
if modes and tables:
|
||||
with st.spinner("Получаю данные..."):
|
||||
data = {
|
||||
"getter": "get_data",
|
||||
"modes": modes,
|
||||
"tables": tables
|
||||
}
|
||||
|
||||
result, status = make_api_request("/svodka_ca/get_data", data)
|
||||
result, status = make_api_request("/svodka_ca/get_ca_data", data)
|
||||
|
||||
if status == 200:
|
||||
st.success("✅ Данные получены")
|
||||
st.json(result)
|
||||
else:
|
||||
st.error(f"❌ Ошибка: {result.get('message', f'Неизвестная ошибка: {status}')}")
|
||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||
else:
|
||||
st.warning("⚠️ Выберите режимы и таблицы")
|
||||
|
||||
@@ -336,9 +291,6 @@ def main():
|
||||
with tab3:
|
||||
st.header("⛽ Мониторинг топлива - Полный функционал")
|
||||
|
||||
# Получаем информацию о геттерах
|
||||
getters_info = get_parser_getters("monitoring_fuel")
|
||||
|
||||
# Секция загрузки файлов
|
||||
st.subheader("📤 Загрузка файлов")
|
||||
uploaded_fuel = st.file_uploader(
|
||||
@@ -367,15 +319,6 @@ def main():
|
||||
# Секция получения данных
|
||||
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)
|
||||
|
||||
with col1:
|
||||
@@ -392,11 +335,10 @@ def main():
|
||||
if columns_fuel:
|
||||
with st.spinner("Получаю данные..."):
|
||||
data = {
|
||||
"getter": "total_by_columns",
|
||||
"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:
|
||||
st.success("✅ Данные получены")
|
||||
@@ -418,22 +360,49 @@ def main():
|
||||
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
|
||||
with st.spinner("Получаю данные..."):
|
||||
data = {
|
||||
"getter": "month_by_code",
|
||||
"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:
|
||||
st.success("✅ Данные получены")
|
||||
st.json(result)
|
||||
else:
|
||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||
|
||||
# Новая секция для временных рядов
|
||||
st.markdown("---")
|
||||
st.subheader("📈 Временные ряды по ID и колонкам")
|
||||
|
||||
columns_series = st.multiselect(
|
||||
"Выберите столбцы для временных рядов",
|
||||
["normativ", "total", "total_1"],
|
||||
default=["normativ", "total"],
|
||||
key="fuel_series_columns"
|
||||
)
|
||||
|
||||
if st.button("📈 Получить временные ряды", key="fuel_series_btn"):
|
||||
if columns_series:
|
||||
with st.spinner("Получаю временные ряды..."):
|
||||
data = {
|
||||
"columns": columns_series
|
||||
}
|
||||
|
||||
result, status = make_api_request("/monitoring_fuel/get_series_by_id_and_columns", data)
|
||||
|
||||
if status == 200:
|
||||
st.success("✅ Временные ряды получены")
|
||||
st.json(result)
|
||||
else:
|
||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||
else:
|
||||
st.warning("⚠️ Выберите столбцы")
|
||||
|
||||
# Футер
|
||||
st.markdown("---")
|
||||
st.markdown("### 📚 Документация API")
|
||||
st.markdown(f"Полная документация доступна по адресу: {API_BASE_URL}/docs")
|
||||
st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs")
|
||||
|
||||
# Информация о проекте
|
||||
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()
|
||||
123
tests/README.md
Normal file
123
tests/README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# API Endpoints Tests
|
||||
|
||||
Этот модуль содержит pytest тесты для всех API эндпоинтов проекта NIN Excel Parsers.
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
tests/
|
||||
├── __init__.py
|
||||
├── conftest.py # Конфигурация pytest
|
||||
├── test_all_endpoints.py # Основной файл для запуска всех тестов
|
||||
├── test_upload_endpoints.py # Тесты API эндпоинтов загрузки данных
|
||||
├── test_svodka_pm_endpoints.py # Тесты API svodka_pm эндпоинтов
|
||||
├── test_svodka_ca_endpoints.py # Тесты API svodka_ca эндпоинтов
|
||||
├── test_monitoring_fuel_endpoints.py # Тесты API monitoring_fuel эндпоинтов
|
||||
├── test_parsers_direct.py # Прямое тестирование парсеров
|
||||
├── test_upload_with_local_storage.py # Тестирование загрузки в локальный storage
|
||||
├── test_getters_with_local_storage.py # Тестирование геттеров с локальными данными
|
||||
├── test_data/ # Тестовые данные
|
||||
│ ├── svodka_ca.xlsx
|
||||
│ ├── pm_plan.zip
|
||||
│ └── monitoring.zip
|
||||
├── local_storage/ # Локальный storage (создается автоматически)
|
||||
│ ├── data/ # Сохраненные DataFrame
|
||||
│ └── metadata/ # Метаданные объектов
|
||||
├── requirements.txt # Зависимости для тестов
|
||||
└── README.md # Этот файл
|
||||
```
|
||||
|
||||
## Установка зависимостей
|
||||
|
||||
```bash
|
||||
pip install -r tests/requirements.txt
|
||||
```
|
||||
|
||||
## Запуск тестов
|
||||
|
||||
### Запуск всех тестов
|
||||
```bash
|
||||
cd tests
|
||||
python test_all_endpoints.py
|
||||
```
|
||||
|
||||
### Запуск конкретных тестов
|
||||
```bash
|
||||
# API тесты (требуют запущенный сервер)
|
||||
pytest test_upload_endpoints.py -v
|
||||
pytest test_svodka_pm_endpoints.py -v
|
||||
pytest test_svodka_ca_endpoints.py -v
|
||||
pytest test_monitoring_fuel_endpoints.py -v
|
||||
|
||||
# Прямые тесты парсеров (не требуют сервер)
|
||||
pytest test_parsers_direct.py -v
|
||||
pytest test_upload_with_local_storage.py -v
|
||||
pytest test_getters_with_local_storage.py -v
|
||||
|
||||
# Все тесты с локальным storage
|
||||
pytest test_parsers_direct.py test_upload_with_local_storage.py test_getters_with_local_storage.py -v
|
||||
```
|
||||
|
||||
## Предварительные условия
|
||||
|
||||
1. **API сервер должен быть запущен** на `http://localhost:8000` (только для API тестов)
|
||||
2. **Тестовые данные** находятся в папке `test_data/`
|
||||
3. **Локальный storage** используется для прямого тестирования парсеров
|
||||
|
||||
## Последовательность тестирования
|
||||
|
||||
### Вариант 1: API тесты (требуют запущенный сервер)
|
||||
1. **Загрузка данных** (`test_upload_endpoints.py`)
|
||||
- Загрузка `svodka_ca.xlsx`
|
||||
- Загрузка `pm_plan.zip`
|
||||
- Загрузка `monitoring.zip`
|
||||
|
||||
2. **Тестирование эндпоинтов** (в любом порядке)
|
||||
- `test_svodka_pm_endpoints.py`
|
||||
- `test_svodka_ca_endpoints.py`
|
||||
- `test_monitoring_fuel_endpoints.py`
|
||||
|
||||
### Вариант 2: Прямые тесты (не требуют сервер)
|
||||
1. **Тестирование парсеров** (`test_parsers_direct.py`)
|
||||
- Проверка регистрации парсеров
|
||||
- Проверка локального storage
|
||||
|
||||
2. **Загрузка в локальный storage** (`test_upload_with_local_storage.py`)
|
||||
- Загрузка всех файлов в локальный storage
|
||||
- Проверка сохранения данных
|
||||
|
||||
3. **Тестирование геттеров** (`test_getters_with_local_storage.py`)
|
||||
- Тестирование всех геттеров с локальными данными
|
||||
- Выявление проблем в логике парсеров
|
||||
|
||||
## Ожидаемые результаты
|
||||
|
||||
Все тесты должны возвращать **статус 200** и содержать поле `"success": true` в ответе.
|
||||
|
||||
## Примеры тестовых запросов
|
||||
|
||||
Тесты используют примеры из Pydantic схем:
|
||||
|
||||
### svodka_pm
|
||||
```json
|
||||
{
|
||||
"id": "SNPZ",
|
||||
"codes": [78, 79],
|
||||
"columns": ["ПП", "СЭБ"]
|
||||
}
|
||||
```
|
||||
|
||||
### svodka_ca
|
||||
```json
|
||||
{
|
||||
"modes": ["fact", "plan"],
|
||||
"tables": ["table1", "table2"]
|
||||
}
|
||||
```
|
||||
|
||||
### monitoring_fuel
|
||||
```json
|
||||
{
|
||||
"columns": ["total", "normativ"]
|
||||
}
|
||||
```
|
||||
71
tests/TEST_RESULTS.md
Normal file
71
tests/TEST_RESULTS.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Результаты тестирования API эндпоинтов
|
||||
|
||||
## Сводка
|
||||
|
||||
Создана полная система тестирования с локальным storage для проверки всех API эндпоинтов проекта NIN Excel Parsers.
|
||||
|
||||
## Структура тестов
|
||||
|
||||
### 1. Прямые тесты парсеров (`test_parsers_direct.py`)
|
||||
- ✅ **Регистрация парсеров** - все парсеры корректно регистрируются
|
||||
- ✅ **Локальный storage** - работает корректно
|
||||
- ✅ **ReportService** - корректно работает с локальным storage
|
||||
|
||||
### 2. Тесты загрузки (`test_upload_with_local_storage.py`)
|
||||
- ❌ **svodka_ca.xlsx** - парсер возвращает `None`
|
||||
- ❌ **pm_plan.zip** - парсер возвращает словарь с `None` значениями
|
||||
- ❌ **monitoring.zip** - парсер возвращает пустой словарь
|
||||
|
||||
### 3. Тесты геттеров (`test_getters_with_local_storage.py`)
|
||||
- ❌ **Все геттеры** - не работают из-за проблем с загрузкой данных
|
||||
|
||||
### 4. API тесты (`test_*_endpoints.py`)
|
||||
- ✅ **Загрузка файлов** - эндпоинты работают
|
||||
- ❌ **Геттеры** - не работают из-за проблем с данными
|
||||
|
||||
## Выявленные проблемы
|
||||
|
||||
### 1. Парсер svodka_ca
|
||||
- **Проблема**: Возвращает `None` вместо DataFrame
|
||||
- **Причина**: Парсер не может обработать тестовый файл `svodka_ca.xlsx`
|
||||
- **Статус**: Требует исправления
|
||||
|
||||
### 2. Парсер svodka_pm
|
||||
- **Проблема**: Возвращает словарь с `None` значениями
|
||||
- **Причина**: Файлы в архиве `pm_plan.zip` не найдены (неправильные имена файлов)
|
||||
- **Статус**: Требует исправления логики поиска файлов
|
||||
|
||||
### 3. Парсер monitoring_fuel
|
||||
- **Проблема**: Возвращает пустой словарь
|
||||
- **Причина**: Ошибки при загрузке файлов - "None of ['id'] are in the columns"
|
||||
- **Статус**: Требует исправления логики обработки колонок
|
||||
|
||||
## Рекомендации
|
||||
|
||||
### Немедленные действия
|
||||
1. **Исправить парсер svodka_ca** - проверить логику парсинга Excel файлов
|
||||
2. **Исправить парсер svodka_pm** - проверить логику поиска файлов в архиве
|
||||
3. **Исправить парсер monitoring_fuel** - проверить логику обработки колонок
|
||||
|
||||
### Долгосрочные улучшения
|
||||
1. **Улучшить обработку ошибок** в парсерах
|
||||
2. **Добавить валидацию данных** перед сохранением
|
||||
3. **Создать более детальные тесты** для каждого парсера
|
||||
|
||||
## Техническая информация
|
||||
|
||||
### Локальный storage
|
||||
- ✅ Создан `LocalStorageAdapter` для тестирования
|
||||
- ✅ Поддерживает все операции: save, load, delete, list
|
||||
- ✅ Автоматически очищается после тестов
|
||||
|
||||
### Инфраструктура тестов
|
||||
- ✅ Pytest конфигурация с фикстурами
|
||||
- ✅ Автоматическая регистрация парсеров
|
||||
- ✅ Поддержка как API, так и прямых тестов
|
||||
|
||||
## Заключение
|
||||
|
||||
Система тестирования создана и работает корректно. Выявлены конкретные проблемы в парсерах, которые требуют исправления. После исправления парсеров все тесты должны пройти успешно.
|
||||
|
||||
**Следующий шаг**: Исправить выявленные проблемы в парсерах согласно результатам отладочных тестов.
|
||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Tests package
|
||||
97
tests/conftest.py
Normal file
97
tests/conftest.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Конфигурация pytest для тестирования API эндпоинтов
|
||||
"""
|
||||
import pytest
|
||||
import requests
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем путь к проекту для импорта модулей
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root / "python_parser"))
|
||||
|
||||
from adapters.local_storage import LocalStorageAdapter
|
||||
|
||||
# Базовый URL API
|
||||
API_BASE_URL = "http://localhost:8000"
|
||||
|
||||
# Путь к тестовым данным
|
||||
TEST_DATA_DIR = Path(__file__).parent / "test_data"
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def api_base_url():
|
||||
"""Базовый URL для API"""
|
||||
return API_BASE_URL
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_data_dir():
|
||||
"""Директория с тестовыми данными"""
|
||||
return TEST_DATA_DIR
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def wait_for_api():
|
||||
"""Ожидание готовности API"""
|
||||
max_attempts = 30
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/docs", timeout=5)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ API готов после {attempt + 1} попыток")
|
||||
return True
|
||||
except requests.exceptions.RequestException:
|
||||
pass
|
||||
|
||||
if attempt < max_attempts - 1:
|
||||
time.sleep(2)
|
||||
|
||||
pytest.fail("❌ API не готов после 30 попыток")
|
||||
|
||||
@pytest.fixture
|
||||
def upload_file(test_data_dir):
|
||||
"""Фикстура для загрузки файла"""
|
||||
def _upload_file(filename):
|
||||
file_path = test_data_dir / filename
|
||||
if not file_path.exists():
|
||||
pytest.skip(f"Файл {filename} не найден в {test_data_dir}")
|
||||
return file_path
|
||||
return _upload_file
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def local_storage():
|
||||
"""Фикстура для локального storage"""
|
||||
storage = LocalStorageAdapter("tests/local_storage")
|
||||
yield storage
|
||||
# Очищаем storage после всех тестов
|
||||
storage.clear_all()
|
||||
|
||||
@pytest.fixture
|
||||
def clean_storage(local_storage):
|
||||
"""Фикстура для очистки storage перед каждым тестом"""
|
||||
local_storage.clear_all()
|
||||
yield local_storage
|
||||
|
||||
def make_api_request(url, method="GET", data=None, files=None, json_data=None):
|
||||
"""Универсальная функция для API запросов"""
|
||||
try:
|
||||
if method.upper() == "GET":
|
||||
response = requests.get(url, timeout=30)
|
||||
elif method.upper() == "POST":
|
||||
if files:
|
||||
response = requests.post(url, files=files, timeout=30)
|
||||
elif json_data:
|
||||
response = requests.post(url, json=json_data, timeout=30)
|
||||
else:
|
||||
response = requests.post(url, data=data, timeout=30)
|
||||
else:
|
||||
raise ValueError(f"Неподдерживаемый метод: {method}")
|
||||
|
||||
return response
|
||||
except requests.exceptions.RequestException as e:
|
||||
pytest.fail(f"Ошибка API запроса: {e}")
|
||||
|
||||
@pytest.fixture
|
||||
def api_request():
|
||||
"""Фикстура для API запросов"""
|
||||
return make_api_request
|
||||
2
tests/requirements.txt
Normal file
2
tests/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pytest>=7.0.0
|
||||
requests>=2.28.0
|
||||
20
tests/test_all_endpoints.py
Normal file
20
tests/test_all_endpoints.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Основной файл для запуска всех тестов API эндпоинтов
|
||||
"""
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем путь к проекту для импорта модулей
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root / "python_parser"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Запуск всех тестов
|
||||
pytest.main([
|
||||
__file__.replace("test_all_endpoints.py", ""),
|
||||
"-v", # подробный вывод
|
||||
"--tb=short", # короткий traceback
|
||||
"--color=yes", # цветной вывод
|
||||
"-x", # остановка на первой ошибке
|
||||
])
|
||||
BIN
tests/test_data/monitoring.zip
Normal file
BIN
tests/test_data/monitoring.zip
Normal file
Binary file not shown.
BIN
tests/test_data/pm_plan.zip
Normal file
BIN
tests/test_data/pm_plan.zip
Normal file
Binary file not shown.
339
tests/test_getters_with_local_storage.py
Normal file
339
tests/test_getters_with_local_storage.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
Тестирование геттеров с данными из локального storage
|
||||
"""
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем путь к проекту
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root / "python_parser"))
|
||||
|
||||
from core.services import ReportService, PARSERS
|
||||
from core.models import DataRequest, UploadRequest
|
||||
from adapters.local_storage import LocalStorageAdapter
|
||||
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser
|
||||
|
||||
# Регистрируем парсеры
|
||||
PARSERS.update({
|
||||
'svodka_pm': SvodkaPMParser,
|
||||
'svodka_ca': SvodkaCAParser,
|
||||
'monitoring_fuel': MonitoringFuelParser,
|
||||
})
|
||||
|
||||
|
||||
class TestGettersWithLocalStorage:
|
||||
"""Тестирование геттеров с локальным storage"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_storage(self, clean_storage):
|
||||
"""Настройка локального storage для каждого теста"""
|
||||
self.storage = clean_storage
|
||||
self.report_service = ReportService(self.storage)
|
||||
|
||||
def test_svodka_pm_single_og_with_local_data(self, upload_file):
|
||||
"""Тест svodka_pm single_og с данными из локального storage"""
|
||||
# Сначала загружаем данные
|
||||
file_path = upload_file("pm_plan.zip")
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
request = UploadRequest(
|
||||
report_type='svodka_pm',
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
upload_result = self.report_service.upload_report(request)
|
||||
assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}"
|
||||
|
||||
# Теперь тестируем геттер
|
||||
data_request = DataRequest(
|
||||
report_type='svodka_pm',
|
||||
get_params={
|
||||
'mode': 'single_og',
|
||||
'id': 'SNPZ',
|
||||
'codes': [78, 79],
|
||||
'columns': ['ПП', 'СЭБ']
|
||||
}
|
||||
)
|
||||
|
||||
result = self.report_service.get_data(data_request)
|
||||
|
||||
if result.success:
|
||||
print(f"✅ svodka_pm/single_og работает с локальными данными")
|
||||
print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}")
|
||||
else:
|
||||
print(f"❌ svodka_pm/single_og не работает: {result.message}")
|
||||
# Не делаем assert, чтобы увидеть все ошибки
|
||||
|
||||
def test_svodka_pm_total_ogs_with_local_data(self, upload_file):
|
||||
"""Тест svodka_pm total_ogs с данными из локального storage"""
|
||||
# Сначала загружаем данные
|
||||
file_path = upload_file("pm_plan.zip")
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
request = UploadRequest(
|
||||
report_type='svodka_pm',
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
upload_result = self.report_service.upload_report(request)
|
||||
assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}"
|
||||
|
||||
# Теперь тестируем геттер
|
||||
data_request = DataRequest(
|
||||
report_type='svodka_pm',
|
||||
get_params={
|
||||
'mode': 'total_ogs',
|
||||
'codes': [78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
|
||||
'columns': ['БП', 'ПП', 'СЭБ']
|
||||
}
|
||||
)
|
||||
|
||||
result = self.report_service.get_data(data_request)
|
||||
|
||||
if result.success:
|
||||
print(f"✅ svodka_pm/total_ogs работает с локальными данными")
|
||||
print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}")
|
||||
else:
|
||||
print(f"❌ svodka_pm/total_ogs не работает: {result.message}")
|
||||
|
||||
def test_svodka_ca_get_ca_data_with_local_data(self, upload_file):
|
||||
"""Тест svodka_ca get_ca_data с данными из локального storage"""
|
||||
# Сначала загружаем данные
|
||||
file_path = upload_file("svodka_ca.xlsx")
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
request = UploadRequest(
|
||||
report_type='svodka_ca',
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
upload_result = self.report_service.upload_report(request)
|
||||
assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}"
|
||||
|
||||
# Теперь тестируем геттер
|
||||
data_request = DataRequest(
|
||||
report_type='svodka_ca',
|
||||
get_params={
|
||||
'mode': 'get_ca_data',
|
||||
'modes': ['fact', 'plan'],
|
||||
'tables': ['table1', 'table2']
|
||||
}
|
||||
)
|
||||
|
||||
result = self.report_service.get_data(data_request)
|
||||
|
||||
if result.success:
|
||||
print(f"✅ svodka_ca/get_ca_data работает с локальными данными")
|
||||
print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}")
|
||||
else:
|
||||
print(f"❌ svodka_ca/get_ca_data не работает: {result.message}")
|
||||
|
||||
def test_monitoring_fuel_get_total_by_columns_with_local_data(self, upload_file):
|
||||
"""Тест monitoring_fuel get_total_by_columns с данными из локального storage"""
|
||||
# Сначала загружаем данные
|
||||
file_path = upload_file("monitoring.zip")
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
request = UploadRequest(
|
||||
report_type='monitoring_fuel',
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
upload_result = self.report_service.upload_report(request)
|
||||
assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}"
|
||||
|
||||
# Теперь тестируем геттер
|
||||
data_request = DataRequest(
|
||||
report_type='monitoring_fuel',
|
||||
get_params={
|
||||
'mode': 'total_by_columns',
|
||||
'columns': ['total', 'normativ']
|
||||
}
|
||||
)
|
||||
|
||||
result = self.report_service.get_data(data_request)
|
||||
|
||||
if result.success:
|
||||
print(f"✅ monitoring_fuel/get_total_by_columns работает с локальными данными")
|
||||
print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}")
|
||||
else:
|
||||
print(f"❌ monitoring_fuel/get_total_by_columns не работает: {result.message}")
|
||||
|
||||
def test_monitoring_fuel_get_month_by_code_with_local_data(self, upload_file):
|
||||
"""Тест monitoring_fuel get_month_by_code с данными из локального storage"""
|
||||
# Сначала загружаем данные
|
||||
file_path = upload_file("monitoring.zip")
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
request = UploadRequest(
|
||||
report_type='monitoring_fuel',
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
upload_result = self.report_service.upload_report(request)
|
||||
assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}"
|
||||
|
||||
# Теперь тестируем геттер
|
||||
data_request = DataRequest(
|
||||
report_type='monitoring_fuel',
|
||||
get_params={
|
||||
'mode': 'month_by_code',
|
||||
'month': '02'
|
||||
}
|
||||
)
|
||||
|
||||
result = self.report_service.get_data(data_request)
|
||||
|
||||
if result.success:
|
||||
print(f"✅ monitoring_fuel/get_month_by_code работает с локальными данными")
|
||||
print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}")
|
||||
else:
|
||||
print(f"❌ monitoring_fuel/get_month_by_code не работает: {result.message}")
|
||||
|
||||
def test_monitoring_fuel_get_series_by_id_and_columns_with_local_data(self, upload_file):
|
||||
"""Тест monitoring_fuel get_series_by_id_and_columns с данными из локального storage"""
|
||||
# Сначала загружаем данные
|
||||
file_path = upload_file("monitoring.zip")
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
request = UploadRequest(
|
||||
report_type='monitoring_fuel',
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
upload_result = self.report_service.upload_report(request)
|
||||
assert upload_result.success is True, f"Загрузка не удалась: {upload_result.message}"
|
||||
|
||||
# Теперь тестируем геттер
|
||||
data_request = DataRequest(
|
||||
report_type='monitoring_fuel',
|
||||
get_params={
|
||||
'mode': 'series_by_id_and_columns',
|
||||
'columns': ['total', 'normativ']
|
||||
}
|
||||
)
|
||||
|
||||
result = self.report_service.get_data(data_request)
|
||||
|
||||
if result.success:
|
||||
print(f"✅ monitoring_fuel/get_series_by_id_and_columns работает с локальными данными")
|
||||
print(f" Получено данных: {len(result.data) if isinstance(result.data, list) else 'не список'}")
|
||||
else:
|
||||
print(f"❌ monitoring_fuel/get_series_by_id_and_columns не работает: {result.message}")
|
||||
|
||||
def test_all_getters_with_loaded_data(self, upload_file):
|
||||
"""Тест всех геттеров с предварительно загруженными данными"""
|
||||
# Загружаем все данные
|
||||
files_to_upload = [
|
||||
("svodka_ca.xlsx", "svodka_ca", "file"),
|
||||
("pm_plan.zip", "svodka_pm", "zip"),
|
||||
("monitoring.zip", "monitoring_fuel", "zip")
|
||||
]
|
||||
|
||||
for filename, report_type, upload_type in files_to_upload:
|
||||
file_path = upload_file(filename)
|
||||
|
||||
# Читаем файл и создаем UploadRequest
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
upload_request = UploadRequest(
|
||||
report_type=report_type,
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
result = self.report_service.upload_report(upload_request)
|
||||
|
||||
assert result.success is True, f"Загрузка {filename} не удалась: {result.message}"
|
||||
print(f"✅ {filename} загружен")
|
||||
|
||||
# Тестируем все геттеры
|
||||
test_cases = [
|
||||
# svodka_pm
|
||||
{
|
||||
'report_type': 'svodka_pm',
|
||||
'mode': 'single_og',
|
||||
'params': {'id': 'SNPZ', 'codes': [78, 79], 'columns': ['ПП', 'СЭБ']},
|
||||
'name': 'svodka_pm/single_og'
|
||||
},
|
||||
{
|
||||
'report_type': 'svodka_pm',
|
||||
'mode': 'total_ogs',
|
||||
'params': {'codes': [78, 79, 394, 395, 396, 397, 81, 82, 83, 84], 'columns': ['БП', 'ПП', 'СЭБ']},
|
||||
'name': 'svodka_pm/total_ogs'
|
||||
},
|
||||
# svodka_ca
|
||||
{
|
||||
'report_type': 'svodka_ca',
|
||||
'mode': 'get_ca_data',
|
||||
'params': {'modes': ['fact', 'plan'], 'tables': ['table1', 'table2']},
|
||||
'name': 'svodka_ca/get_ca_data'
|
||||
},
|
||||
# monitoring_fuel
|
||||
{
|
||||
'report_type': 'monitoring_fuel',
|
||||
'mode': 'total_by_columns',
|
||||
'params': {'columns': ['total', 'normativ']},
|
||||
'name': 'monitoring_fuel/get_total_by_columns'
|
||||
},
|
||||
{
|
||||
'report_type': 'monitoring_fuel',
|
||||
'mode': 'month_by_code',
|
||||
'params': {'month': '02'},
|
||||
'name': 'monitoring_fuel/get_month_by_code'
|
||||
},
|
||||
{
|
||||
'report_type': 'monitoring_fuel',
|
||||
'mode': 'series_by_id_and_columns',
|
||||
'params': {'columns': ['total', 'normativ']},
|
||||
'name': 'monitoring_fuel/get_series_by_id_and_columns'
|
||||
}
|
||||
]
|
||||
|
||||
print("\n🧪 Тестирование всех геттеров с локальными данными:")
|
||||
|
||||
for test_case in test_cases:
|
||||
request_params = test_case['params'].copy()
|
||||
request_params['mode'] = test_case['mode']
|
||||
|
||||
data_request = DataRequest(
|
||||
report_type=test_case['report_type'],
|
||||
get_params=request_params
|
||||
)
|
||||
|
||||
result = self.report_service.get_data(data_request)
|
||||
|
||||
if result.success:
|
||||
print(f"✅ {test_case['name']}: работает")
|
||||
else:
|
||||
print(f"❌ {test_case['name']}: {result.message}")
|
||||
|
||||
# Показываем содержимое storage
|
||||
objects = self.storage.list_objects()
|
||||
print(f"\n📊 Объекты в локальном storage: {len(objects)}")
|
||||
for obj_id in objects:
|
||||
metadata = self.storage.get_object_metadata(obj_id)
|
||||
if metadata:
|
||||
print(f" 📁 {obj_id}: {metadata['shape']} колонки: {metadata['columns'][:3]}...")
|
||||
102
tests/test_monitoring_fuel_endpoints.py
Normal file
102
tests/test_monitoring_fuel_endpoints.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Тесты для monitoring_fuel эндпоинтов
|
||||
"""
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
class TestMonitoringFuelEndpoints:
|
||||
"""Тесты эндпоинтов monitoring_fuel"""
|
||||
|
||||
def test_monitoring_fuel_get_total_by_columns(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных по колонкам и расчёт средних значений"""
|
||||
# Пример из схемы MonitoringFuelTotalRequest
|
||||
data = {
|
||||
"columns": ["total", "normativ"]
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/monitoring_fuel/get_total_by_columns", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ monitoring_fuel/get_total_by_columns работает: получены данные для колонок {data['columns']}")
|
||||
|
||||
def test_monitoring_fuel_get_month_by_code(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных за месяц"""
|
||||
# Пример из схемы MonitoringFuelMonthRequest
|
||||
data = {
|
||||
"month": "02"
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/monitoring_fuel/get_month_by_code", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ monitoring_fuel/get_month_by_code работает: получены данные за месяц {data['month']}")
|
||||
|
||||
def test_monitoring_fuel_get_series_by_id_and_columns(self, wait_for_api, api_base_url):
|
||||
"""Тест получения временных рядов по ID и колонкам"""
|
||||
# Пример из схемы MonitoringFuelSeriesRequest
|
||||
data = {
|
||||
"columns": ["total", "normativ"]
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/monitoring_fuel/get_series_by_id_and_columns", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ monitoring_fuel/get_series_by_id_and_columns работает: получены временные ряды для колонок {data['columns']}")
|
||||
|
||||
def test_monitoring_fuel_get_total_by_columns_single_column(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных по одной колонке"""
|
||||
data = {
|
||||
"columns": ["total"]
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/monitoring_fuel/get_total_by_columns", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ monitoring_fuel/get_total_by_columns с одной колонкой работает: получены данные для колонки {data['columns'][0]}")
|
||||
|
||||
def test_monitoring_fuel_get_month_by_code_different_month(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных за другой месяц"""
|
||||
data = {
|
||||
"month": "01"
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/monitoring_fuel/get_month_by_code", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ monitoring_fuel/get_month_by_code с другим месяцем работает: получены данные за месяц {data['month']}")
|
||||
|
||||
def test_monitoring_fuel_get_series_by_id_and_columns_single_column(self, wait_for_api, api_base_url):
|
||||
"""Тест получения временных рядов по одной колонке"""
|
||||
data = {
|
||||
"columns": ["total"]
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/monitoring_fuel/get_series_by_id_and_columns", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ monitoring_fuel/get_series_by_id_and_columns с одной колонкой работает: получены временные ряды для колонки {data['columns'][0]}")
|
||||
134
tests/test_parsers_direct.py
Normal file
134
tests/test_parsers_direct.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Прямое тестирование парсеров с локальным storage
|
||||
Этот модуль тестирует парсеры напрямую, без API
|
||||
"""
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем путь к проекту
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root / "python_parser"))
|
||||
|
||||
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser
|
||||
from core.services import ReportService
|
||||
from adapters.local_storage import LocalStorageAdapter
|
||||
|
||||
|
||||
class TestParsersDirect:
|
||||
"""Прямое тестирование парсеров с локальным storage"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_storage(self, clean_storage):
|
||||
"""Настройка локального storage для каждого теста"""
|
||||
self.storage = clean_storage
|
||||
self.report_service = ReportService(self.storage)
|
||||
|
||||
def test_svodka_pm_parser_registration(self):
|
||||
"""Тест регистрации парсера svodka_pm"""
|
||||
parser = SvodkaPMParser()
|
||||
getters = parser.get_available_getters()
|
||||
|
||||
assert "single_og" in getters
|
||||
assert "total_ogs" in getters
|
||||
|
||||
# Проверяем параметры геттеров
|
||||
single_og_getter = getters["single_og"]
|
||||
assert "id" in single_og_getter["required_params"]
|
||||
assert "codes" in single_og_getter["required_params"]
|
||||
assert "columns" in single_og_getter["required_params"]
|
||||
assert "search" in single_og_getter["optional_params"]
|
||||
|
||||
total_ogs_getter = getters["total_ogs"]
|
||||
assert "codes" in total_ogs_getter["required_params"]
|
||||
assert "columns" in total_ogs_getter["required_params"]
|
||||
assert "search" in total_ogs_getter["optional_params"]
|
||||
|
||||
print("✅ svodka_pm парсер зарегистрирован корректно")
|
||||
|
||||
def test_svodka_ca_parser_registration(self):
|
||||
"""Тест регистрации парсера svodka_ca"""
|
||||
parser = SvodkaCAParser()
|
||||
getters = parser.get_available_getters()
|
||||
|
||||
assert "get_ca_data" in getters
|
||||
|
||||
# Проверяем параметры геттера
|
||||
getter = getters["get_ca_data"]
|
||||
assert "modes" in getter["required_params"]
|
||||
assert "tables" in getter["required_params"]
|
||||
|
||||
print("✅ svodka_ca парсер зарегистрирован корректно")
|
||||
|
||||
def test_monitoring_fuel_parser_registration(self):
|
||||
"""Тест регистрации парсера monitoring_fuel"""
|
||||
parser = MonitoringFuelParser()
|
||||
getters = parser.get_available_getters()
|
||||
|
||||
assert "total_by_columns" in getters
|
||||
assert "month_by_code" in getters
|
||||
assert "series_by_id_and_columns" in getters
|
||||
|
||||
# Проверяем параметры геттеров
|
||||
total_getter = getters["total_by_columns"]
|
||||
assert "columns" in total_getter["required_params"]
|
||||
|
||||
month_getter = getters["month_by_code"]
|
||||
assert "month" in month_getter["required_params"]
|
||||
|
||||
series_getter = getters["series_by_id_and_columns"]
|
||||
assert "columns" in series_getter["required_params"]
|
||||
|
||||
print("✅ monitoring_fuel парсер зарегистрирован корректно")
|
||||
|
||||
def test_storage_operations(self):
|
||||
"""Тест операций с локальным storage"""
|
||||
import pandas as pd
|
||||
|
||||
# Создаем тестовый DataFrame
|
||||
test_df = pd.DataFrame({
|
||||
'col1': [1, 2, 3],
|
||||
'col2': ['a', 'b', 'c']
|
||||
})
|
||||
|
||||
# Сохраняем
|
||||
success = self.storage.save_dataframe("test_object", test_df)
|
||||
assert success is True
|
||||
|
||||
# Проверяем существование
|
||||
exists = self.storage.object_exists("test_object")
|
||||
assert exists is True
|
||||
|
||||
# Загружаем
|
||||
loaded_df = self.storage.load_dataframe("test_object")
|
||||
assert loaded_df is not None
|
||||
assert loaded_df.shape == (3, 2)
|
||||
assert list(loaded_df.columns) == ['col1', 'col2']
|
||||
|
||||
# Получаем метаданные
|
||||
metadata = self.storage.get_object_metadata("test_object")
|
||||
assert metadata is not None
|
||||
assert metadata["shape"] == [3, 2]
|
||||
|
||||
# Получаем список объектов
|
||||
objects = self.storage.list_objects()
|
||||
assert "test_object" in objects
|
||||
|
||||
# Удаляем
|
||||
delete_success = self.storage.delete_object("test_object")
|
||||
assert delete_success is True
|
||||
|
||||
# Проверяем, что объект удален
|
||||
exists_after = self.storage.object_exists("test_object")
|
||||
assert exists_after is False
|
||||
|
||||
print("✅ Локальный storage работает корректно")
|
||||
|
||||
def test_report_service_with_local_storage(self):
|
||||
"""Тест ReportService с локальным storage"""
|
||||
# Проверяем, что ReportService может работать с локальным storage
|
||||
assert self.report_service.storage is not None
|
||||
assert hasattr(self.report_service.storage, 'save_dataframe')
|
||||
assert hasattr(self.report_service.storage, 'load_dataframe')
|
||||
|
||||
print("✅ ReportService корректно работает с локальным storage")
|
||||
58
tests/test_svodka_ca_endpoints.py
Normal file
58
tests/test_svodka_ca_endpoints.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Тесты для svodka_ca эндпоинтов
|
||||
"""
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
class TestSvodkaCAEndpoints:
|
||||
"""Тесты эндпоинтов svodka_ca"""
|
||||
|
||||
def test_svodka_ca_get_ca_data(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных из сводок СА"""
|
||||
# Пример из схемы SvodkaCARequest
|
||||
data = {
|
||||
"modes": ["fact", "plan"],
|
||||
"tables": ["table1", "table2"]
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/svodka_ca/get_ca_data", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ svodka_ca/get_ca_data работает: получены данные для режимов {data['modes']}")
|
||||
|
||||
def test_svodka_ca_get_ca_data_single_mode(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных из сводок СА для одного режима"""
|
||||
data = {
|
||||
"modes": ["fact"],
|
||||
"tables": ["table1"]
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/svodka_ca/get_ca_data", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ svodka_ca/get_ca_data с одним режимом работает: получены данные для режима {data['modes'][0]}")
|
||||
|
||||
def test_svodka_ca_get_ca_data_multiple_tables(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных из сводок СА для нескольких таблиц"""
|
||||
data = {
|
||||
"modes": ["fact", "plan"],
|
||||
"tables": ["table1", "table2", "table3"]
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/svodka_ca/get_ca_data", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ svodka_ca/get_ca_data с несколькими таблицами работает: получены данные для {len(data['tables'])} таблиц")
|
||||
79
tests/test_svodka_pm_endpoints.py
Normal file
79
tests/test_svodka_pm_endpoints.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Тесты для svodka_pm эндпоинтов
|
||||
"""
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
class TestSvodkaPMEndpoints:
|
||||
"""Тесты эндпоинтов svodka_pm"""
|
||||
|
||||
def test_svodka_pm_single_og(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных по одному ОГ"""
|
||||
# Пример из схемы SvodkaPMSingleOGRequest
|
||||
data = {
|
||||
"id": "SNPZ",
|
||||
"codes": [78, 79],
|
||||
"columns": ["ПП", "СЭБ"]
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/svodka_pm/single_og", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ svodka_pm/single_og работает: получены данные для {data['id']}")
|
||||
|
||||
def test_svodka_pm_total_ogs(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных по всем ОГ"""
|
||||
# Пример из схемы SvodkaPMTotalOGsRequest
|
||||
data = {
|
||||
"codes": [78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
|
||||
"columns": ["БП", "ПП", "СЭБ"]
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/svodka_pm/get_total_ogs", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ svodka_pm/get_total_ogs работает: получены данные по всем ОГ")
|
||||
|
||||
def test_svodka_pm_single_og_with_search(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных по одному ОГ с параметром search"""
|
||||
data = {
|
||||
"id": "SNPZ",
|
||||
"codes": [78, 79],
|
||||
"columns": ["ПП", "СЭБ"],
|
||||
"search": "Итого"
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/svodka_pm/single_og", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ svodka_pm/single_og с search работает: получены данные для {data['id']} с фильтром")
|
||||
|
||||
def test_svodka_pm_total_ogs_with_search(self, wait_for_api, api_base_url):
|
||||
"""Тест получения данных по всем ОГ с параметром search"""
|
||||
data = {
|
||||
"codes": [78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
|
||||
"columns": ["БП", "ПП", "СЭБ"],
|
||||
"search": "Итого"
|
||||
}
|
||||
|
||||
response = requests.post(f"{api_base_url}/svodka_pm/get_total_ogs", json=data)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Запрос не удался: {result}"
|
||||
assert "data" in result, "Отсутствует поле 'data' в ответе"
|
||||
print(f"✅ svodka_pm/get_total_ogs с search работает: получены данные по всем ОГ с фильтром")
|
||||
52
tests/test_upload_endpoints.py
Normal file
52
tests/test_upload_endpoints.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
Тесты для эндпоинтов загрузки данных
|
||||
"""
|
||||
import pytest
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class TestUploadEndpoints:
|
||||
"""Тесты эндпоинтов загрузки"""
|
||||
|
||||
def test_upload_svodka_ca(self, wait_for_api, upload_file, api_base_url):
|
||||
"""Тест загрузки файла svodka_ca.xlsx"""
|
||||
file_path = upload_file("svodka_ca.xlsx")
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
files = {'file': ('svodka_ca.xlsx', f, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
|
||||
response = requests.post(f"{api_base_url}/svodka_ca/upload", files=files)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Загрузка не удалась: {result}"
|
||||
print(f"✅ svodka_ca.xlsx загружен успешно: {result['message']}")
|
||||
|
||||
def test_upload_svodka_pm_plan(self, wait_for_api, upload_file, api_base_url):
|
||||
"""Тест загрузки архива pm_plan.zip"""
|
||||
file_path = upload_file("pm_plan.zip")
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
files = {'zip_file': ('pm_plan.zip', f, 'application/zip')}
|
||||
response = requests.post(f"{api_base_url}/svodka_pm/upload-zip", files=files)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Загрузка не удалась: {result}"
|
||||
print(f"✅ pm_plan.zip загружен успешно: {result['message']}")
|
||||
|
||||
def test_upload_monitoring_fuel(self, wait_for_api, upload_file, api_base_url):
|
||||
"""Тест загрузки архива monitoring.zip"""
|
||||
file_path = upload_file("monitoring.zip")
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
files = {'zip_file': ('monitoring.zip', f, 'application/zip')}
|
||||
response = requests.post(f"{api_base_url}/monitoring_fuel/upload-zip", files=files)
|
||||
|
||||
assert response.status_code == 200, f"Ожидался статус 200, получен {response.status_code}: {response.text}"
|
||||
|
||||
result = response.json()
|
||||
assert result["success"] is True, f"Загрузка не удалась: {result}"
|
||||
print(f"✅ monitoring.zip загружен успешно: {result['message']}")
|
||||
183
tests/test_upload_with_local_storage.py
Normal file
183
tests/test_upload_with_local_storage.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
Тестирование загрузки файлов с сохранением в локальный storage
|
||||
"""
|
||||
import pytest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем путь к проекту
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root / "python_parser"))
|
||||
|
||||
from core.services import ReportService, PARSERS
|
||||
from core.models import UploadRequest
|
||||
from adapters.local_storage import LocalStorageAdapter
|
||||
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser
|
||||
|
||||
# Регистрируем парсеры
|
||||
PARSERS.update({
|
||||
'svodka_pm': SvodkaPMParser,
|
||||
'svodka_ca': SvodkaCAParser,
|
||||
'monitoring_fuel': MonitoringFuelParser,
|
||||
})
|
||||
|
||||
|
||||
class TestUploadWithLocalStorage:
|
||||
"""Тестирование загрузки файлов с локальным storage"""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_storage(self, clean_storage):
|
||||
"""Настройка локального storage для каждого теста"""
|
||||
self.storage = clean_storage
|
||||
self.report_service = ReportService(self.storage)
|
||||
|
||||
def test_upload_svodka_ca_to_local_storage(self, upload_file):
|
||||
"""Тест загрузки svodka_ca.xlsx в локальный storage"""
|
||||
file_path = upload_file("svodka_ca.xlsx")
|
||||
|
||||
# Читаем файл и создаем UploadRequest
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
request = UploadRequest(
|
||||
report_type='svodka_ca',
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
# Загружаем файл через ReportService
|
||||
result = self.report_service.upload_report(request)
|
||||
|
||||
assert result.success is True, f"Загрузка не удалась: {result.message}"
|
||||
|
||||
# Проверяем, что данные сохранились в локальном storage
|
||||
objects = self.storage.list_objects()
|
||||
assert len(objects) > 0, "Данные не сохранились в storage"
|
||||
|
||||
# Проверяем метаданные
|
||||
for obj_id in objects:
|
||||
metadata = self.storage.get_object_metadata(obj_id)
|
||||
assert metadata is not None, f"Метаданные для {obj_id} не найдены"
|
||||
assert "shape" in metadata, f"Отсутствует shape в метаданных {obj_id}"
|
||||
assert "columns" in metadata, f"Отсутствуют columns в метаданных {obj_id}"
|
||||
|
||||
print(f"✅ svodka_ca.xlsx загружен в локальный storage: {len(objects)} объектов")
|
||||
print(f" Объекты: {objects}")
|
||||
|
||||
def test_upload_pm_plan_to_local_storage(self, upload_file):
|
||||
"""Тест загрузки pm_plan.zip в локальный storage"""
|
||||
file_path = upload_file("pm_plan.zip")
|
||||
|
||||
# Читаем файл и создаем UploadRequest
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
request = UploadRequest(
|
||||
report_type='svodka_pm',
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
# Загружаем архив через ReportService
|
||||
result = self.report_service.upload_report(request)
|
||||
|
||||
assert result.success is True, f"Загрузка не удалась: {result.message}"
|
||||
|
||||
# Проверяем, что данные сохранились в локальном storage
|
||||
objects = self.storage.list_objects()
|
||||
assert len(objects) > 0, "Данные не сохранились в storage"
|
||||
|
||||
# Проверяем метаданные
|
||||
for obj_id in objects:
|
||||
metadata = self.storage.get_object_metadata(obj_id)
|
||||
assert metadata is not None, f"Метаданные для {obj_id} не найдены"
|
||||
assert "shape" in metadata, f"Отсутствует shape в метаданных {obj_id}"
|
||||
assert "columns" in metadata, f"Отсутствуют columns в метаданных {obj_id}"
|
||||
|
||||
print(f"✅ pm_plan.zip загружен в локальный storage: {len(objects)} объектов")
|
||||
print(f" Объекты: {objects}")
|
||||
|
||||
def test_upload_monitoring_to_local_storage(self, upload_file):
|
||||
"""Тест загрузки monitoring.zip в локальный storage"""
|
||||
file_path = upload_file("monitoring.zip")
|
||||
|
||||
# Читаем файл и создаем UploadRequest
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
request = UploadRequest(
|
||||
report_type='monitoring_fuel',
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
# Загружаем архив через ReportService
|
||||
result = self.report_service.upload_report(request)
|
||||
|
||||
assert result.success is True, f"Загрузка не удалась: {result.message}"
|
||||
|
||||
# Проверяем, что данные сохранились в локальном storage
|
||||
objects = self.storage.list_objects()
|
||||
assert len(objects) > 0, "Данные не сохранились в storage"
|
||||
|
||||
# Проверяем метаданные
|
||||
for obj_id in objects:
|
||||
metadata = self.storage.get_object_metadata(obj_id)
|
||||
assert metadata is not None, f"Метаданные для {obj_id} не найдены"
|
||||
assert "shape" in metadata, f"Отсутствует shape в метаданных {obj_id}"
|
||||
assert "columns" in metadata, f"Отсутствуют columns в метаданных {obj_id}"
|
||||
|
||||
print(f"✅ monitoring.zip загружен в локальный storage: {len(objects)} объектов")
|
||||
print(f" Объекты: {objects}")
|
||||
|
||||
def test_upload_all_files_sequence(self, upload_file):
|
||||
"""Тест последовательной загрузки всех файлов"""
|
||||
# Загружаем все файлы по очереди
|
||||
files_to_upload = [
|
||||
("svodka_ca.xlsx", "svodka_ca", "file"),
|
||||
("pm_plan.zip", "svodka_pm", "zip"),
|
||||
("monitoring.zip", "monitoring_fuel", "zip")
|
||||
]
|
||||
|
||||
total_objects = 0
|
||||
|
||||
for filename, report_type, upload_type in files_to_upload:
|
||||
file_path = upload_file(filename)
|
||||
|
||||
# Читаем файл и создаем UploadRequest
|
||||
with open(file_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
request = UploadRequest(
|
||||
report_type=report_type,
|
||||
file_name=file_path.name,
|
||||
file_content=file_content,
|
||||
parse_params={}
|
||||
)
|
||||
|
||||
result = self.report_service.upload_report(request)
|
||||
|
||||
assert result.success is True, f"Загрузка {filename} не удалась: {result.message}"
|
||||
|
||||
# Подсчитываем объекты
|
||||
objects = self.storage.list_objects()
|
||||
current_count = len(objects)
|
||||
|
||||
print(f"✅ {filename} загружен: {current_count - total_objects} новых объектов")
|
||||
total_objects = current_count
|
||||
|
||||
# Проверяем итоговое количество объектов
|
||||
final_objects = self.storage.list_objects()
|
||||
assert len(final_objects) > 0, "Ни один файл не был загружен"
|
||||
|
||||
print(f"✅ Все файлы загружены. Итого объектов в storage: {len(final_objects)}")
|
||||
print(f" Все объекты: {final_objects}")
|
||||
|
||||
# Выводим детальную информацию о каждом объекте
|
||||
for obj_id in final_objects:
|
||||
metadata = self.storage.get_object_metadata(obj_id)
|
||||
if metadata:
|
||||
print(f" 📊 {obj_id}: {metadata['shape']} колонки: {metadata['columns'][:5]}...")
|
||||
Reference in New Issue
Block a user