Compare commits
14 Commits
9459196804
...
upd_exist_
| Author | SHA1 | Date | |
|---|---|---|---|
| de63f98b50 | |||
| 84069e4e41 | |||
| b8074765e3 | |||
| 79ab91c700 | |||
| b98be22359 | |||
| fc0b4356da | |||
| 72fe115a99 | |||
| 46a30c32ed | |||
| 5e217c7cce | |||
| 7d2747c8fe | |||
| 513ff3c144 | |||
| a0b6e04d99 | |||
| 47a7344755 | |||
| 456e9935f0 |
167
.gitignore
vendored
167
.gitignore
vendored
@@ -1,13 +1,12 @@
|
|||||||
data/
|
# Python
|
||||||
# Byte-compiled / optimized / DLL files
|
__pycache__
|
||||||
__pycache__/
|
*.pyc
|
||||||
|
|
||||||
|
nin_python_parser
|
||||||
|
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
@@ -20,26 +19,82 @@ lib64/
|
|||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
# Installer logs
|
# Virtual environments
|
||||||
pip-log.txt
|
.env
|
||||||
pip-delete-this-directory.txt
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
# Unit test / coverage reports
|
# IDE
|
||||||
htmlcov/
|
.vscode/
|
||||||
.tox/
|
.idea/
|
||||||
.nox/
|
*.swp
|
||||||
.coverage
|
*.swo
|
||||||
.coverage.*
|
*~
|
||||||
.cache
|
|
||||||
nosetests.xml
|
# OS
|
||||||
coverage.xml
|
.DS_Store
|
||||||
*.cover
|
.DS_Store?
|
||||||
.hypothesis/
|
._*
|
||||||
|
.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/
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
@@ -47,6 +102,29 @@ coverage.xml
|
|||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# Celery
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
.dmypy.json
|
.dmypy.json
|
||||||
@@ -55,36 +133,27 @@ dmypy.json
|
|||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
# VS Code
|
# pytype static type analyzer
|
||||||
.vscode/
|
.pytype/
|
||||||
|
|
||||||
# PyCharm
|
# Cython debug symbols
|
||||||
.idea/
|
cython_debug/
|
||||||
|
|
||||||
# Local envs
|
# Local development
|
||||||
.env
|
local_settings.py
|
||||||
.venv
|
db.sqlite3
|
||||||
env/
|
db.sqlite3-journal
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# MacOS
|
# FastAPI
|
||||||
.DS_Store
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
# Windows
|
# Streamlit
|
||||||
Thumbs.db
|
.streamlit/secrets.toml
|
||||||
ehthumbs.db
|
|
||||||
Desktop.ini
|
|
||||||
|
|
||||||
# MinIO test data
|
# Node.js (if any frontend components)
|
||||||
minio_data/
|
node_modules/
|
||||||
minio_test/
|
npm-debug.log*
|
||||||
minio/
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Streamlit cache
|
|
||||||
.streamlit/
|
|
||||||
|
|||||||
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 будет скачивать образы и собирать контейнеры, это может занять несколько минут.
|
||||||
117
README.md
Normal file
117
README.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Python Parser CF - Система анализа данных
|
||||||
|
|
||||||
|
Проект состоит из трех основных компонентов:
|
||||||
|
- **python_parser** - FastAPI приложение для парсинга и обработки данных
|
||||||
|
- **streamlit_app** - Streamlit приложение для визуализации и анализа
|
||||||
|
- **minio_data** - хранилище данных MinIO
|
||||||
|
|
||||||
|
## 🚀 Быстрый запуск
|
||||||
|
|
||||||
|
### Предварительные требования
|
||||||
|
- Docker и Docker Compose
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Запуск всех сервисов (продакшн)
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск в режиме разработки
|
||||||
|
```bash
|
||||||
|
# Автоматический запуск
|
||||||
|
python start_dev.py
|
||||||
|
|
||||||
|
# Или вручную
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Режим разработки** позволяет:
|
||||||
|
- Автоматически перезагружать Streamlit при изменении кода
|
||||||
|
- Монтировать исходный код напрямую в контейнер
|
||||||
|
- Видеть изменения без пересборки контейнеров
|
||||||
|
|
||||||
|
### Доступ к сервисам
|
||||||
|
- **FastAPI**: http://localhost:8000
|
||||||
|
- **Streamlit**: http://localhost:8501
|
||||||
|
- **MinIO Console**: http://localhost:9001
|
||||||
|
- **MinIO API**: http://localhost:9000
|
||||||
|
|
||||||
|
### Остановка сервисов
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
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 # Документация
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Конфигурация
|
||||||
|
|
||||||
|
### Переменные окружения
|
||||||
|
Все сервисы используют следующие переменные окружения:
|
||||||
|
- `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. **Запустите все сервисы**: `docker-compose up -d`
|
||||||
|
2. **Откройте Streamlit**: http://localhost:8501
|
||||||
|
3. **Выберите тип данных** для анализа
|
||||||
|
4. **Просматривайте результаты** в интерактивном интерфейсе
|
||||||
|
|
||||||
|
## 🛠️ Разработка
|
||||||
|
|
||||||
|
### Режим разработки (рекомендуется)
|
||||||
|
```bash
|
||||||
|
# Запуск режима разработки
|
||||||
|
python start_dev.py
|
||||||
|
|
||||||
|
# Остановка
|
||||||
|
docker compose -f docker-compose.dev.yml down
|
||||||
|
|
||||||
|
# Возврат к продакшн режиму
|
||||||
|
python start_prod.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Локальная разработка FastAPI
|
||||||
|
```bash
|
||||||
|
cd python_parser
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Локальная разработка Streamlit
|
||||||
|
```bash
|
||||||
|
cd streamlit_app
|
||||||
|
pip install -r requirements.txt
|
||||||
|
streamlit run streamlit_app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Лицензия
|
||||||
|
|
||||||
|
Проект разработан для внутреннего использования.
|
||||||
58
docker-compose.dev.yml
Normal file
58
docker-compose.dev.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
services:
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
container_name: svodka_minio_dev
|
||||||
|
ports:
|
||||||
|
- "9000:9000" # API порт
|
||||||
|
- "9001:9001" # Консоль порт
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD: minioadmin
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
volumes:
|
||||||
|
- ./minio_data:/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
fastapi:
|
||||||
|
build: ./python_parser
|
||||||
|
container_name: svodka_fastapi_dev
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
environment:
|
||||||
|
- MINIO_ENDPOINT=minio:9000
|
||||||
|
- MINIO_ACCESS_KEY=minioadmin
|
||||||
|
- MINIO_SECRET_KEY=minioadmin
|
||||||
|
- MINIO_SECURE=false
|
||||||
|
- MINIO_BUCKET=svodka-data
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
streamlit:
|
||||||
|
image: python:3.11-slim
|
||||||
|
container_name: svodka_streamlit_dev
|
||||||
|
ports:
|
||||||
|
- "8501:8501"
|
||||||
|
environment:
|
||||||
|
- API_BASE_URL=http://fastapi:8000
|
||||||
|
- API_PUBLIC_URL=http://localhost:8000
|
||||||
|
- MINIO_ENDPOINT=minio:9000
|
||||||
|
- MINIO_ACCESS_KEY=minioadmin
|
||||||
|
- MINIO_SECRET_KEY=minioadmin
|
||||||
|
- MINIO_SECURE=false
|
||||||
|
- MINIO_BUCKET=svodka-data
|
||||||
|
volumes:
|
||||||
|
# Монтируем исходный код для автоматической перезагрузки
|
||||||
|
- ./streamlit_app:/app
|
||||||
|
# Монтируем requirements.txt для установки зависимостей
|
||||||
|
- ./streamlit_app/requirements.txt:/app/requirements.txt
|
||||||
|
working_dir: /app
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
- fastapi
|
||||||
|
restart: unless-stopped
|
||||||
|
command: >
|
||||||
|
bash -c "
|
||||||
|
pip install --no-cache-dir -r requirements.txt &&
|
||||||
|
streamlit run streamlit_app.py --server.port=8501 --server.address=0.0.0.0 --server.runOnSave=true
|
||||||
|
"
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Продакшн конфигурация
|
||||||
|
# Для разработки используйте: docker compose -f docker-compose.dev.yml up -d
|
||||||
services:
|
services:
|
||||||
minio:
|
minio:
|
||||||
image: minio/minio:latest
|
image: minio/minio:latest
|
||||||
@@ -10,11 +12,11 @@ services:
|
|||||||
MINIO_ROOT_PASSWORD: minioadmin
|
MINIO_ROOT_PASSWORD: minioadmin
|
||||||
command: server /data --console-address ":9001"
|
command: server /data --console-address ":9001"
|
||||||
volumes:
|
volumes:
|
||||||
- minio_data:/data
|
- ./minio_data:/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
fastapi:
|
fastapi:
|
||||||
build: .
|
build: ./python_parser
|
||||||
container_name: svodka_fastapi
|
container_name: svodka_fastapi
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
@@ -35,9 +37,13 @@ services:
|
|||||||
- "8501:8501"
|
- "8501:8501"
|
||||||
environment:
|
environment:
|
||||||
- API_BASE_URL=http://fastapi:8000
|
- 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
|
||||||
depends_on:
|
depends_on:
|
||||||
|
- minio
|
||||||
- fastapi
|
- fastapi
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
|
||||||
minio_data:
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
[server]
|
|
||||||
port = 8501
|
|
||||||
address = "localhost"
|
|
||||||
headless = false
|
|
||||||
enableCORS = false
|
|
||||||
enableXsrfProtection = false
|
|
||||||
|
|
||||||
[browser]
|
|
||||||
gatherUsageStats = false
|
|
||||||
serverAddress = "localhost"
|
|
||||||
serverPort = 8501
|
|
||||||
|
|
||||||
[theme]
|
|
||||||
primaryColor = "#FF4B4B"
|
|
||||||
backgroundColor = "#FFFFFF"
|
|
||||||
secondaryBackgroundColor = "#F0F2F6"
|
|
||||||
textColor = "#262730"
|
|
||||||
font = "sans serif"
|
|
||||||
|
|
||||||
[client]
|
|
||||||
showErrorDetails = true
|
|
||||||
caching = true
|
|
||||||
displayEnabled = true
|
|
||||||
|
|
||||||
[runner]
|
|
||||||
magicEnabled = true
|
|
||||||
installTracer = false
|
|
||||||
fixMatplotlib = true
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
web: python /app/run_stand.py
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# 🚀 Быстрый старт NIN Excel Parsers API
|
|
||||||
|
|
||||||
## 🐳 Запуск через Docker (рекомендуется)
|
|
||||||
|
|
||||||
### Вариант 1: MinIO + FastAPI в Docker
|
|
||||||
```bash
|
|
||||||
# Запуск всех сервисов
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Проверка
|
|
||||||
curl http://localhost:8000
|
|
||||||
curl http://localhost:9001
|
|
||||||
```
|
|
||||||
|
|
||||||
### Вариант 2: Только MinIO в Docker
|
|
||||||
```bash
|
|
||||||
# Запуск только MinIO
|
|
||||||
docker-compose up -d minio
|
|
||||||
|
|
||||||
# Проверка
|
|
||||||
curl http://localhost:9001
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🖥️ Запуск FastAPI локально
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Если MinIO в Docker
|
|
||||||
python run_dev.py
|
|
||||||
|
|
||||||
# Проверка
|
|
||||||
curl http://localhost:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Запуск Streamlit
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# В отдельном терминале
|
|
||||||
python run_streamlit.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 Доступные URL
|
|
||||||
|
|
||||||
- **FastAPI API**: http://localhost:8000
|
|
||||||
- **API документация**: http://localhost:8000/docs
|
|
||||||
- **MinIO консоль**: http://localhost:9001
|
|
||||||
- **Streamlit интерфейс**: http://localhost:8501
|
|
||||||
|
|
||||||
## 🛑 Остановка
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Остановка Docker
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Остановка Streamlit
|
|
||||||
# Ctrl+C в терминале
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Диагностика
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Проверка состояния
|
|
||||||
python check_services.py
|
|
||||||
|
|
||||||
# Просмотр логов Docker
|
|
||||||
docker-compose logs
|
|
||||||
```
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# NIN Excel Parsers API
|
|
||||||
|
|
||||||
API для парсинга Excel отчетов нефтеперерабатывающих заводов (НПЗ) с использованием FastAPI и MinIO для хранения данных.
|
|
||||||
|
|
||||||
## 🚀 Быстрый запуск
|
|
||||||
|
|
||||||
### **Вариант 1: Все сервисы в Docker (рекомендуется)**
|
|
||||||
```bash
|
|
||||||
# Запуск всех сервисов: MinIO + FastAPI + Streamlit
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Доступ:
|
|
||||||
# - MinIO Console: http://localhost:9001
|
|
||||||
# - FastAPI: http://localhost:8000
|
|
||||||
# - Streamlit: http://localhost:8501
|
|
||||||
# - API Docs: http://localhost:8000/docs
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Вариант 2: Только MinIO в Docker + FastAPI локально**
|
|
||||||
```bash
|
|
||||||
# Запуск MinIO в Docker
|
|
||||||
docker-compose up -d minio
|
|
||||||
|
|
||||||
# Запуск FastAPI локально
|
|
||||||
python run_dev.py
|
|
||||||
|
|
||||||
# В отдельном терминале запуск Streamlit
|
|
||||||
cd streamlit_app
|
|
||||||
streamlit run app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Вариант 3: Только MinIO в Docker**
|
|
||||||
```bash
|
|
||||||
# Запуск только MinIO
|
|
||||||
docker-compose up -d minio
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Описание сервисов
|
|
||||||
|
|
||||||
- **MinIO** (порт 9000-9001): S3-совместимое хранилище для данных
|
|
||||||
- **FastAPI** (порт 8000): API сервер для парсинга Excel файлов
|
|
||||||
- **Streamlit** (порт 8501): Веб-интерфейс для демонстрации API
|
|
||||||
|
|
||||||
## 🛑 Остановка
|
|
||||||
|
|
||||||
### Остановка Docker сервисов:
|
|
||||||
```bash
|
|
||||||
# Все сервисы
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Только MinIO
|
|
||||||
docker-compose stop minio
|
|
||||||
```
|
|
||||||
|
|
||||||
### Остановка локальных сервисов:
|
|
||||||
```bash
|
|
||||||
# Нажмите Ctrl+C в терминале с FastAPI/Streamlit
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Структура проекта
|
|
||||||
|
|
||||||
```
|
|
||||||
python_parser/
|
|
||||||
├── app/ # FastAPI приложение
|
|
||||||
│ ├── main.py # Основной файл приложения
|
|
||||||
│ └── schemas/ # Pydantic схемы
|
|
||||||
├── core/ # Бизнес-логика
|
|
||||||
│ ├── models.py # Модели данных
|
|
||||||
│ ├── ports.py # Интерфейсы (порты)
|
|
||||||
│ └── services.py # Сервисы
|
|
||||||
├── adapters/ # Адаптеры для внешних систем
|
|
||||||
│ ├── storage.py # MinIO адаптер
|
|
||||||
│ └── parsers/ # Парсеры Excel файлов
|
|
||||||
├── streamlit_app/ # Изолированный Streamlit пакет
|
|
||||||
│ ├── app.py # Основное Streamlit приложение
|
|
||||||
│ ├── requirements.txt # Зависимости Streamlit
|
|
||||||
│ ├── Dockerfile # Docker образ для Streamlit
|
|
||||||
│ └── .streamlit/ # Конфигурация Streamlit
|
|
||||||
├── data/ # Тестовые данные
|
|
||||||
├── docker-compose.yml # Docker Compose конфигурация
|
|
||||||
├── Dockerfile # Docker образ для FastAPI
|
|
||||||
└── run_dev.py # Запуск FastAPI локально
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Доступные эндпоинты
|
|
||||||
|
|
||||||
- **GET /** - Информация об API
|
|
||||||
- **GET /docs** - Swagger документация
|
|
||||||
- **GET /parsers** - Список доступных парсеров
|
|
||||||
- **GET /parsers/{parser_name}/getters** - Информация о геттерах парсера
|
|
||||||
- **POST /svodka_pm/upload-zip** - Загрузка сводок ПМ
|
|
||||||
- **POST /svodka_ca/upload** - Загрузка сводок ЦА
|
|
||||||
- **POST /monitoring_fuel/upload-zip** - Загрузка мониторинга топлива
|
|
||||||
- **POST /svodka_pm/get_data** - Получение данных сводок ПМ
|
|
||||||
- **POST /svodka_ca/get_data** - Получение данных сводок ЦА
|
|
||||||
- **POST /monitoring_fuel/get_data** - Получение данных мониторинга топлива
|
|
||||||
|
|
||||||
## 📊 Поддерживаемые типы отчетов
|
|
||||||
|
|
||||||
1. **svodka_pm** - Сводки по переработке нефти (ПМ)
|
|
||||||
- Геттеры: `single_og`, `total_ogs`
|
|
||||||
2. **svodka_ca** - Сводки по переработке нефти (ЦА)
|
|
||||||
- Геттеры: `get_data`
|
|
||||||
3. **monitoring_fuel** - Мониторинг топлива
|
|
||||||
- Геттеры: `total_by_columns`, `month_by_code`
|
|
||||||
|
|
||||||
## 🏗️ Архитектура
|
|
||||||
|
|
||||||
Проект использует **Hexagonal Architecture (Ports and Adapters)**:
|
|
||||||
|
|
||||||
- **Порты (Ports)**: Интерфейсы для бизнес-логики
|
|
||||||
- **Адаптеры (Adapters)**: Реализации для внешних систем
|
|
||||||
- **Сервисы (Services)**: Бизнес-логика приложения
|
|
||||||
|
|
||||||
### Система геттеров парсеров
|
|
||||||
|
|
||||||
Каждый парсер может иметь несколько методов получения данных (геттеров):
|
|
||||||
- Регистрация геттеров в словаре с метаданными
|
|
||||||
- Валидация параметров для каждого геттера
|
|
||||||
- Единый интерфейс `get_value(getter_name, params)`
|
|
||||||
|
|
||||||
## 🐳 Docker
|
|
||||||
|
|
||||||
### Сборка образов:
|
|
||||||
```bash
|
|
||||||
# FastAPI
|
|
||||||
docker build -t nin-fastapi .
|
|
||||||
|
|
||||||
# Streamlit
|
|
||||||
docker build -t nin-streamlit ./streamlit_app
|
|
||||||
```
|
|
||||||
|
|
||||||
### Запуск отдельных сервисов:
|
|
||||||
```bash
|
|
||||||
# Только MinIO
|
|
||||||
docker-compose up -d minio
|
|
||||||
|
|
||||||
# MinIO + FastAPI
|
|
||||||
docker-compose up -d minio fastapi
|
|
||||||
|
|
||||||
# Все сервисы
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
# 🚀 Streamlit Demo для NIN Excel Parsers API
|
|
||||||
|
|
||||||
## Описание
|
|
||||||
|
|
||||||
Streamlit приложение для демонстрации работы всех API эндпоинтов NIN Excel Parsers. Предоставляет удобный веб-интерфейс для тестирования функциональности парсеров.
|
|
||||||
|
|
||||||
## Возможности
|
|
||||||
|
|
||||||
- 📤 **Загрузка файлов**: Загрузка ZIP архивов и Excel файлов
|
|
||||||
- 📊 **Сводки ПМ**: Работа с плановыми и фактическими данными
|
|
||||||
- 🏭 **Сводки СА**: Парсинг сводок центрального аппарата
|
|
||||||
- ⛽ **Мониторинг топлива**: Анализ данных по топливу
|
|
||||||
- 📱 **Адаптивный интерфейс**: Удобное использование на всех устройствах
|
|
||||||
|
|
||||||
## Установка и запуск
|
|
||||||
|
|
||||||
### 1. Установка зависимостей
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Запуск FastAPI сервера
|
|
||||||
|
|
||||||
В одном терминале:
|
|
||||||
```bash
|
|
||||||
python run_dev.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Запуск Streamlit приложения
|
|
||||||
|
|
||||||
В другом терминале:
|
|
||||||
```bash
|
|
||||||
python run_streamlit.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Или напрямую:
|
|
||||||
```bash
|
|
||||||
streamlit run streamlit_app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Открытие в браузере
|
|
||||||
|
|
||||||
Приложение автоматически откроется по адресу: http://localhost:8501
|
|
||||||
|
|
||||||
## Конфигурация
|
|
||||||
|
|
||||||
### Переменные окружения
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# URL API сервера
|
|
||||||
export API_BASE_URL="http://localhost:8000"
|
|
||||||
|
|
||||||
# Порт Streamlit
|
|
||||||
export STREAMLIT_PORT="8501"
|
|
||||||
|
|
||||||
# Хост Streamlit
|
|
||||||
export STREAMLIT_HOST="localhost"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Настройки Streamlit
|
|
||||||
|
|
||||||
Файл `.streamlit/config.toml` содержит настройки:
|
|
||||||
- Порт: 8501
|
|
||||||
- Хост: localhost
|
|
||||||
- Тема: Кастомная цветовая схема
|
|
||||||
- Безопасность: Отключены CORS и XSRF для локальной разработки
|
|
||||||
|
|
||||||
## Структура приложения
|
|
||||||
|
|
||||||
### Вкладки
|
|
||||||
|
|
||||||
1. **📤 Загрузка файлов**
|
|
||||||
- Загрузка сводок ПМ (ZIP)
|
|
||||||
- Загрузка мониторинга топлива (ZIP)
|
|
||||||
- Загрузка сводки СА (Excel)
|
|
||||||
|
|
||||||
2. **📊 Сводки ПМ**
|
|
||||||
- Данные по одному ОГ
|
|
||||||
- Данные по всем ОГ
|
|
||||||
- Выбор кодов строк и столбцов
|
|
||||||
|
|
||||||
3. **🏭 Сводки СА**
|
|
||||||
- Выбор режимов (план/факт/норматив)
|
|
||||||
- Выбор таблиц для анализа
|
|
||||||
|
|
||||||
4. **⛽ Мониторинг топлива**
|
|
||||||
- Агрегация по колонкам
|
|
||||||
- Данные за конкретный месяц
|
|
||||||
|
|
||||||
### Боковая панель
|
|
||||||
|
|
||||||
- Информация о сервере (PID, CPU, память)
|
|
||||||
- Список доступных парсеров
|
|
||||||
- Статус подключения к API
|
|
||||||
|
|
||||||
## Использование
|
|
||||||
|
|
||||||
### 1. Загрузка файлов
|
|
||||||
|
|
||||||
1. Выберите соответствующий тип файла
|
|
||||||
2. Нажмите "Загрузить"
|
|
||||||
3. Дождитесь подтверждения загрузки
|
|
||||||
|
|
||||||
### 2. Получение данных
|
|
||||||
|
|
||||||
1. Выберите нужные параметры (ОГ, коды, столбцы)
|
|
||||||
2. Нажмите "Получить данные"
|
|
||||||
3. Результат отобразится в JSON формате
|
|
||||||
|
|
||||||
### 3. Мониторинг
|
|
||||||
|
|
||||||
- Проверяйте статус API в верхней части
|
|
||||||
- Следите за логами операций
|
|
||||||
- Используйте индикаторы загрузки
|
|
||||||
|
|
||||||
## Устранение неполадок
|
|
||||||
|
|
||||||
### API недоступен
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Проверьте, запущен ли FastAPI сервер
|
|
||||||
curl http://localhost:8000/
|
|
||||||
|
|
||||||
# Проверьте порт
|
|
||||||
netstat -an | grep 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Streamlit не запускается
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Проверьте версию Python
|
|
||||||
python --version
|
|
||||||
|
|
||||||
# Переустановите Streamlit
|
|
||||||
pip uninstall streamlit
|
|
||||||
pip install streamlit
|
|
||||||
|
|
||||||
# Проверьте порт 8501
|
|
||||||
netstat -an | grep 8501
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ошибки загрузки файлов
|
|
||||||
|
|
||||||
- Убедитесь, что файл соответствует формату
|
|
||||||
- Проверьте размер файла (не более 100MB)
|
|
||||||
- Убедитесь, что MinIO запущен
|
|
||||||
|
|
||||||
## Разработка
|
|
||||||
|
|
||||||
### Добавление новых функций
|
|
||||||
|
|
||||||
1. Создайте новую вкладку в `streamlit_app.py`
|
|
||||||
2. Добавьте соответствующие API вызовы
|
|
||||||
3. Обновите боковую панель при необходимости
|
|
||||||
|
|
||||||
### Кастомизация темы
|
|
||||||
|
|
||||||
Отредактируйте `.streamlit/config.toml`:
|
|
||||||
```toml
|
|
||||||
[theme]
|
|
||||||
primaryColor = "#FF4B4B"
|
|
||||||
backgroundColor = "#FFFFFF"
|
|
||||||
# ... другие цвета
|
|
||||||
```
|
|
||||||
|
|
||||||
### Добавление новых парсеров
|
|
||||||
|
|
||||||
1. Создайте парсер в `adapters/parsers/`
|
|
||||||
2. Добавьте в `main.py`
|
|
||||||
3. Обновите Streamlit интерфейс
|
|
||||||
|
|
||||||
## Безопасность
|
|
||||||
|
|
||||||
⚠️ **Внимание**: Приложение настроено для локальной разработки
|
|
||||||
- CORS отключен
|
|
||||||
- XSRF защита отключена
|
|
||||||
- Не используйте в продакшене без дополнительной настройки
|
|
||||||
|
|
||||||
## Поддержка
|
|
||||||
|
|
||||||
При возникновении проблем:
|
|
||||||
1. Проверьте логи в терминале
|
|
||||||
2. Убедитесь, что все сервисы запущены
|
|
||||||
3. Проверьте конфигурацию
|
|
||||||
4. Обратитесь к документации API: http://localhost:8000/docs
|
|
||||||
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
|
import zipfile
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple
|
||||||
from core.ports import ParserPort
|
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):
|
class MonitoringFuelParser(ParserPort):
|
||||||
@@ -13,37 +15,48 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
|
|
||||||
def _register_default_getters(self):
|
def _register_default_getters(self):
|
||||||
"""Регистрация геттеров по умолчанию"""
|
"""Регистрация геттеров по умолчанию"""
|
||||||
self.register_getter(
|
# Используем схемы Pydantic как единый источник правды
|
||||||
name="total_by_columns",
|
register_getter_from_schema(
|
||||||
|
parser_instance=self,
|
||||||
|
getter_name="total_by_columns",
|
||||||
method=self._get_total_by_columns,
|
method=self._get_total_by_columns,
|
||||||
required_params=["columns"],
|
schema_class=MonitoringFuelTotalRequest,
|
||||||
optional_params=[],
|
|
||||||
description="Агрегация данных по колонкам"
|
description="Агрегация данных по колонкам"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.register_getter(
|
register_getter_from_schema(
|
||||||
name="month_by_code",
|
parser_instance=self,
|
||||||
|
getter_name="month_by_code",
|
||||||
method=self._get_month_by_code,
|
method=self._get_month_by_code,
|
||||||
required_params=["month"],
|
schema_class=MonitoringFuelMonthRequest,
|
||||||
optional_params=[],
|
|
||||||
description="Получение данных за конкретный месяц"
|
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):
|
def _get_total_by_columns(self, params: dict):
|
||||||
"""Агрегация по колонкам (обертка для совместимости)"""
|
"""Агрегация данных по колонкам"""
|
||||||
columns = params["columns"]
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
if not columns:
|
validated_params = validate_params_with_schema(params, MonitoringFuelTotalRequest)
|
||||||
raise ValueError("Отсутствуют идентификаторы столбцов")
|
|
||||||
|
columns = validated_params["columns"]
|
||||||
|
|
||||||
# TODO: Переделать под новую архитектуру
|
# TODO: Переделать под новую архитектуру
|
||||||
df_means, _ = self.aggregate_by_columns(self.df, columns)
|
df_means, _ = self.aggregate_by_columns(self.df, columns)
|
||||||
return df_means.to_dict(orient='index')
|
return df_means.to_dict(orient='index')
|
||||||
|
|
||||||
def _get_month_by_code(self, params: dict):
|
def _get_month_by_code(self, params: dict):
|
||||||
"""Получение данных за месяц (обертка для совместимости)"""
|
"""Получение данных за конкретный месяц"""
|
||||||
month = params["month"]
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
if not month:
|
validated_params = validate_params_with_schema(params, MonitoringFuelMonthRequest)
|
||||||
raise ValueError("Отсутствует идентификатор месяца")
|
|
||||||
|
month = validated_params["month"]
|
||||||
|
|
||||||
# TODO: Переделать под новую архитектуру
|
# TODO: Переделать под новую архитектуру
|
||||||
df_month = self.get_month(self.df, month)
|
df_month = self.get_month(self.df, month)
|
||||||
@@ -87,36 +100,21 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
|
|
||||||
return df_monitorings
|
return df_monitorings
|
||||||
|
|
||||||
def find_header_row(self, file_path: str, sheet: str, search_value: str = "Установка", max_rows: int = 50) -> int:
|
|
||||||
"""Определение индекса заголовка в Excel по ключевому слову"""
|
|
||||||
# Читаем первые max_rows строк без заголовков
|
|
||||||
df_temp = pd.read_excel(
|
|
||||||
file_path,
|
|
||||||
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 + 1 # возвращаем индекс строки (0-based)
|
|
||||||
|
|
||||||
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
|
||||||
|
|
||||||
def parse_single(self, file, sheet, header_num=None):
|
def parse_single(self, file, sheet, header_num=None):
|
||||||
''' Собственно парсер отчетов одного объекта'''
|
''' Собственно парсер отчетов одного объекта'''
|
||||||
# Автоопределение header_num, если не передан
|
# Автоопределение header_num, если не передан
|
||||||
if header_num is None:
|
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(
|
df_full = pd.read_excel(
|
||||||
file,
|
file,
|
||||||
sheet_name=sheet,
|
sheet_name=sheet,
|
||||||
header=header_num,
|
header=header_num,
|
||||||
usecols=None,
|
usecols=None,
|
||||||
index_col=None
|
index_col=None,
|
||||||
|
engine='openpyxl'
|
||||||
)
|
)
|
||||||
|
|
||||||
# === Удаление полностью пустых столбцов ===
|
# === Удаление полностью пустых столбцов ===
|
||||||
@@ -230,3 +228,47 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
total.name = 'mean'
|
total.name = 'mean'
|
||||||
|
|
||||||
return total, df_combined
|
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
|
import numpy as np
|
||||||
|
|
||||||
from core.ports import ParserPort
|
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
|
from adapters.pconfig import get_og_by_name
|
||||||
|
|
||||||
|
|
||||||
@@ -12,23 +14,22 @@ class SvodkaCAParser(ParserPort):
|
|||||||
|
|
||||||
def _register_default_getters(self):
|
def _register_default_getters(self):
|
||||||
"""Регистрация геттеров по умолчанию"""
|
"""Регистрация геттеров по умолчанию"""
|
||||||
self.register_getter(
|
# Используем схемы Pydantic как единый источник правды
|
||||||
name="get_data",
|
register_getter_from_schema(
|
||||||
|
parser_instance=self,
|
||||||
|
getter_name="get_ca_data",
|
||||||
method=self._get_data_wrapper,
|
method=self._get_data_wrapper,
|
||||||
required_params=["modes", "tables"],
|
schema_class=SvodkaCARequest,
|
||||||
optional_params=[],
|
|
||||||
description="Получение данных по режимам и таблицам"
|
description="Получение данных по режимам и таблицам"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_data_wrapper(self, params: dict):
|
def _get_data_wrapper(self, params: dict):
|
||||||
"""Обертка для получения данных (для совместимости)"""
|
"""Получение данных по режимам и таблицам"""
|
||||||
modes = params["modes"]
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
tables = params["tables"]
|
validated_params = validate_params_with_schema(params, SvodkaCARequest)
|
||||||
|
|
||||||
if not isinstance(modes, list):
|
modes = validated_params["modes"]
|
||||||
raise ValueError("Поле 'modes' должно быть списком")
|
tables = validated_params["tables"]
|
||||||
if not isinstance(tables, list):
|
|
||||||
raise ValueError("Поле 'tables' должно быть списком")
|
|
||||||
|
|
||||||
# TODO: Переделать под новую архитектуру
|
# TODO: Переделать под новую архитектуру
|
||||||
data_dict = {}
|
data_dict = {}
|
||||||
@@ -44,6 +45,10 @@ class SvodkaCAParser(ParserPort):
|
|||||||
|
|
||||||
def parse_svodka_ca(self, file_path: str, params: dict) -> dict:
|
def parse_svodka_ca(self, file_path: str, params: dict) -> dict:
|
||||||
"""Парсинг сводки СА"""
|
"""Парсинг сводки СА"""
|
||||||
|
# Получаем параметры из params
|
||||||
|
sheet_name = params.get('sheet_name', 0) # По умолчанию первый лист
|
||||||
|
inclusion_list = params.get('inclusion_list', {'ТиП', 'Топливо', 'Потери'})
|
||||||
|
|
||||||
# === Извлечение и фильтрация ===
|
# === Извлечение и фильтрация ===
|
||||||
tables = self.extract_all_tables(file_path, sheet_name)
|
tables = self.extract_all_tables(file_path, sheet_name)
|
||||||
|
|
||||||
@@ -150,8 +155,8 @@ class SvodkaCAParser(ParserPort):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def extract_all_tables(self, file_path, sheet_name=0):
|
def extract_all_tables(self, file_path, sheet_name=0):
|
||||||
"""Извлекает все таблицы из Excel файла"""
|
"""Извлечение всех таблиц из Excel файла"""
|
||||||
df = pd.read_excel(file_path, sheet_name=sheet_name, header=None)
|
df = pd.read_excel(file_path, sheet_name=sheet_name, header=None, engine='openpyxl')
|
||||||
df_filled = df.fillna('')
|
df_filled = df.fillna('')
|
||||||
df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True)
|
df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from core.ports import ParserPort
|
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):
|
class SvodkaPMParser(ParserPort):
|
||||||
@@ -11,48 +13,45 @@ class SvodkaPMParser(ParserPort):
|
|||||||
|
|
||||||
def _register_default_getters(self):
|
def _register_default_getters(self):
|
||||||
"""Регистрация геттеров по умолчанию"""
|
"""Регистрация геттеров по умолчанию"""
|
||||||
self.register_getter(
|
# Используем схемы Pydantic как единый источник правды
|
||||||
name="single_og",
|
register_getter_from_schema(
|
||||||
|
parser_instance=self,
|
||||||
|
getter_name="single_og",
|
||||||
method=self._get_single_og,
|
method=self._get_single_og,
|
||||||
required_params=["id", "codes", "columns"],
|
schema_class=SvodkaPMSingleOGRequest,
|
||||||
optional_params=["search"],
|
|
||||||
description="Получение данных по одному ОГ"
|
description="Получение данных по одному ОГ"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.register_getter(
|
register_getter_from_schema(
|
||||||
name="total_ogs",
|
parser_instance=self,
|
||||||
|
getter_name="total_ogs",
|
||||||
method=self._get_total_ogs,
|
method=self._get_total_ogs,
|
||||||
required_params=["codes", "columns"],
|
schema_class=SvodkaPMTotalOGsRequest,
|
||||||
optional_params=["search"],
|
|
||||||
description="Получение данных по всем ОГ"
|
description="Получение данных по всем ОГ"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_single_og(self, params: dict):
|
def _get_single_og(self, params: dict):
|
||||||
"""Получение данных по одному ОГ (обертка для совместимости)"""
|
"""Получение данных по одному ОГ"""
|
||||||
og_id = params["id"]
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
codes = params["codes"]
|
validated_params = validate_params_with_schema(params, SvodkaPMSingleOGRequest)
|
||||||
columns = params["columns"]
|
|
||||||
search = params.get("search")
|
|
||||||
|
|
||||||
if not isinstance(codes, list):
|
og_id = validated_params["id"]
|
||||||
raise ValueError("Поле 'codes' должно быть списком")
|
codes = validated_params["codes"]
|
||||||
if not isinstance(columns, list):
|
columns = validated_params["columns"]
|
||||||
raise ValueError("Поле 'columns' должно быть списком")
|
search = validated_params.get("search")
|
||||||
|
|
||||||
# Здесь нужно получить DataFrame из self.df, но пока используем старую логику
|
# Здесь нужно получить DataFrame из self.df, но пока используем старую логику
|
||||||
# TODO: Переделать под новую архитектуру
|
# TODO: Переделать под новую архитектуру
|
||||||
return self.get_svodka_og(self.df, og_id, codes, columns, search)
|
return self.get_svodka_og(self.df, og_id, codes, columns, search)
|
||||||
|
|
||||||
def _get_total_ogs(self, params: dict):
|
def _get_total_ogs(self, params: dict):
|
||||||
"""Получение данных по всем ОГ (обертка для совместимости)"""
|
"""Получение данных по всем ОГ"""
|
||||||
codes = params["codes"]
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
columns = params["columns"]
|
validated_params = validate_params_with_schema(params, SvodkaPMTotalOGsRequest)
|
||||||
search = params.get("search")
|
|
||||||
|
|
||||||
if not isinstance(codes, list):
|
codes = validated_params["codes"]
|
||||||
raise ValueError("Поле 'codes' должно быть списком")
|
columns = validated_params["columns"]
|
||||||
if not isinstance(columns, list):
|
search = validated_params.get("search")
|
||||||
raise ValueError("Поле 'columns' должно быть списком")
|
|
||||||
|
|
||||||
# TODO: Переделать под новую архитектуру
|
# TODO: Переделать под новую архитектуру
|
||||||
return self.get_svodka_total(self.df, codes, columns, search)
|
return self.get_svodka_total(self.df, codes, columns, search)
|
||||||
@@ -63,29 +62,13 @@ class SvodkaPMParser(ParserPort):
|
|||||||
self.df = self.parse_svodka_pm_files(file_path, params)
|
self.df = self.parse_svodka_pm_files(file_path, params)
|
||||||
return self.df
|
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
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
|
||||||
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):
|
def parse_svodka_pm(self, file, sheet, header_num=None):
|
||||||
''' Собственно парсер отчетов одного ОГ для БП, ПП и факта '''
|
''' Собственно парсер отчетов одного ОГ для БП, ПП и факта '''
|
||||||
# Автоопределение header_num, если не передан
|
# Автоопределение header_num, если не передан
|
||||||
if header_num is None:
|
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
|
# Читаем заголовки header_num и 1-2 строки данных, чтобы найти INDICATOR_ID
|
||||||
df_probe = pd.read_excel(
|
df_probe = pd.read_excel(
|
||||||
@@ -94,6 +77,7 @@ class SvodkaPMParser(ParserPort):
|
|||||||
header=header_num,
|
header=header_num,
|
||||||
usecols=None,
|
usecols=None,
|
||||||
nrows=2,
|
nrows=2,
|
||||||
|
engine='openpyxl'
|
||||||
)
|
)
|
||||||
|
|
||||||
if df_probe.shape[0] == 0:
|
if df_probe.shape[0] == 0:
|
||||||
@@ -115,7 +99,8 @@ class SvodkaPMParser(ParserPort):
|
|||||||
sheet_name=sheet,
|
sheet_name=sheet,
|
||||||
header=header_num,
|
header=header_num,
|
||||||
usecols=None,
|
usecols=None,
|
||||||
index_col=None
|
index_col=None,
|
||||||
|
engine='openpyxl'
|
||||||
)
|
)
|
||||||
|
|
||||||
if indicator_col_name not in df_full.columns:
|
if indicator_col_name not in df_full.columns:
|
||||||
@@ -181,7 +166,7 @@ class SvodkaPMParser(ParserPort):
|
|||||||
excel_plan_template = 'svodka_plan_pm_ID.xlsx'
|
excel_plan_template = 'svodka_plan_pm_ID.xlsx'
|
||||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
file_list = zip_ref.namelist()
|
file_list = zip_ref.namelist()
|
||||||
for name, id in OG_IDS.items():
|
for id in SINGLE_OGS:
|
||||||
if id == 'BASH':
|
if id == 'BASH':
|
||||||
continue # пропускаем BASH
|
continue # пропускаем BASH
|
||||||
|
|
||||||
@@ -288,11 +273,11 @@ class SvodkaPMParser(ParserPort):
|
|||||||
''' Служебная функция агрегации данные по всем ОГ '''
|
''' Служебная функция агрегации данные по всем ОГ '''
|
||||||
total_result = {}
|
total_result = {}
|
||||||
|
|
||||||
for name, og_id in OG_IDS.items():
|
for og_id in SINGLE_OGS:
|
||||||
if og_id == 'BASH':
|
if og_id == 'BASH':
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# print(f"📊 Обработка: {name} ({og_id})")
|
# print(f"📊 Обработка: {og_id}")
|
||||||
try:
|
try:
|
||||||
data = self.get_svodka_og(
|
data = self.get_svodka_og(
|
||||||
pm_dict,
|
pm_dict,
|
||||||
@@ -303,7 +288,7 @@ class SvodkaPMParser(ParserPort):
|
|||||||
)
|
)
|
||||||
total_result[og_id] = data
|
total_result[og_id] = data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Ошибка при обработке {name} ({og_id}): {e}")
|
print(f"❌ Ошибка при обработке {og_id}: {e}")
|
||||||
total_result[og_id] = None
|
total_result[og_id] = None
|
||||||
|
|
||||||
return total_result
|
return total_result
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from functools import lru_cache
|
|||||||
import json
|
import json
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import os
|
||||||
|
|
||||||
OG_IDS = {
|
OG_IDS = {
|
||||||
"Комсомольский НПЗ": "KNPZ",
|
"Комсомольский НПЗ": "KNPZ",
|
||||||
@@ -22,8 +23,37 @@ OG_IDS = {
|
|||||||
"Красноленинский НПЗ": "KLNPZ",
|
"Красноленинский НПЗ": "KLNPZ",
|
||||||
"Пурнефтепереработка": "PurNP",
|
"Пурнефтепереработка": "PurNP",
|
||||||
"ЯНОС": "YANOS",
|
"ЯНОС": "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_IDS = {
|
||||||
"Висбрекинг": "SNPZ.VISB",
|
"Висбрекинг": "SNPZ.VISB",
|
||||||
"Изомеризация": "SNPZ.IZOM",
|
"Изомеризация": "SNPZ.IZOM",
|
||||||
@@ -40,7 +70,18 @@ SNPZ_IDS = {
|
|||||||
|
|
||||||
|
|
||||||
def replace_id_in_path(file_path, new_id):
|
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):
|
def get_table_name(exel):
|
||||||
@@ -109,6 +150,25 @@ def get_id_by_name(name, dictionary):
|
|||||||
return best_match
|
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):
|
def data_to_json(data, indent=2, ensure_ascii=False):
|
||||||
"""
|
"""
|
||||||
Полностью безопасная сериализация данных в JSON.
|
Полностью безопасная сериализация данных в JSON.
|
||||||
@@ -175,7 +235,6 @@ def data_to_json(data, indent=2, ensure_ascii=False):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
cleaned_data = convert_obj(data)
|
cleaned_data = convert_obj(data)
|
||||||
cleaned_data_str = json.dumps(cleaned_data, indent=indent, ensure_ascii=ensure_ascii)
|
return json.dumps(cleaned_data, indent=indent, ensure_ascii=ensure_ascii)
|
||||||
return cleaned_data
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Не удалось сериализовать данные в JSON: {e}")
|
raise ValueError(f"Не удалось сериализовать данные в JSON: {e}")
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -16,7 +16,7 @@ from app.schemas import (
|
|||||||
UploadResponse, UploadErrorResponse,
|
UploadResponse, UploadErrorResponse,
|
||||||
SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest,
|
SvodkaPMTotalOGsRequest, SvodkaPMSingleOGRequest,
|
||||||
SvodkaCARequest,
|
SvodkaCARequest,
|
||||||
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest
|
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest, MonitoringFuelSeriesRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -377,7 +377,7 @@ async def get_svodka_pm_total_ogs(
|
|||||||
try:
|
try:
|
||||||
# Создаем запрос
|
# Создаем запрос
|
||||||
request_dict = request_data.model_dump()
|
request_dict = request_data.model_dump()
|
||||||
request_dict['mode'] = 'total'
|
request_dict['mode'] = 'total_ogs'
|
||||||
request = DataRequest(
|
request = DataRequest(
|
||||||
report_type='svodka_pm',
|
report_type='svodka_pm',
|
||||||
get_params=request_dict
|
get_params=request_dict
|
||||||
@@ -400,41 +400,6 @@ async def get_svodka_pm_total_ogs(
|
|||||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
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],
|
@app.post("/svodka_ca/upload", tags=[SvodkaCAParser.name],
|
||||||
summary="Загрузка файла отчета сводки СА",
|
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="Получение данных из отчета сводки СА")
|
summary="Получение данных из отчета сводки СА")
|
||||||
async def get_svodka_ca_data(
|
async def get_svodka_ca_data(
|
||||||
request_data: SvodkaCARequest
|
request_data: SvodkaCARequest
|
||||||
@@ -534,6 +499,7 @@ async def get_svodka_ca_data(
|
|||||||
try:
|
try:
|
||||||
# Создаем запрос
|
# Создаем запрос
|
||||||
request_dict = request_data.model_dump()
|
request_dict = request_data.model_dump()
|
||||||
|
request_dict['mode'] = 'get_ca_data'
|
||||||
request = DataRequest(
|
request = DataRequest(
|
||||||
report_type='svodka_ca',
|
report_type='svodka_ca',
|
||||||
get_params=request_dict
|
get_params=request_dict
|
||||||
@@ -610,38 +576,6 @@ async def get_svodka_ca_data(
|
|||||||
# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
# 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])
|
# @app.post("/monitoring_fuel/upload_directory", tags=[MonitoringFuelParser.name])
|
||||||
@@ -804,7 +738,7 @@ async def get_monitoring_fuel_total_by_columns(
|
|||||||
try:
|
try:
|
||||||
# Создаем запрос
|
# Создаем запрос
|
||||||
request_dict = request_data.model_dump()
|
request_dict = request_data.model_dump()
|
||||||
request_dict['mode'] = 'total'
|
request_dict['mode'] = 'total_by_columns'
|
||||||
request = DataRequest(
|
request = DataRequest(
|
||||||
report_type='monitoring_fuel',
|
report_type='monitoring_fuel',
|
||||||
get_params=request_dict
|
get_params=request_dict
|
||||||
@@ -849,7 +783,56 @@ async def get_monitoring_fuel_month_by_code(
|
|||||||
try:
|
try:
|
||||||
# Создаем запрос
|
# Создаем запрос
|
||||||
request_dict = request_data.model_dump()
|
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(
|
request = DataRequest(
|
||||||
report_type='monitoring_fuel',
|
report_type='monitoring_fuel',
|
||||||
get_params=request_dict
|
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_ca import SvodkaCARequest
|
||||||
from .svodka_pm import SvodkaPMSingleOGRequest, SvodkaPMTotalOGsRequest
|
from .svodka_pm import SvodkaPMSingleOGRequest, SvodkaPMTotalOGsRequest
|
||||||
from .server import ServerInfoResponse
|
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"]
|
"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 {}
|
get_params = request.get_params or {}
|
||||||
|
|
||||||
# Определяем имя геттера (по умолчанию используем первый доступный)
|
# Определяем имя геттера из параметра mode
|
||||||
getter_name = get_params.pop("getter", None)
|
getter_name = get_params.pop("mode", None)
|
||||||
if not getter_name:
|
if not getter_name:
|
||||||
# Если геттер не указан, берем первый доступный
|
# Если режим не указан, берем первый доступный
|
||||||
available_getters = list(parser.getters.keys())
|
available_getters = list(parser.getters.keys())
|
||||||
if available_getters:
|
if available_getters:
|
||||||
getter_name = available_getters[0]
|
getter_name = available_getters[0]
|
||||||
print(f"⚠️ Геттер не указан, используем первый доступный: {getter_name}")
|
print(f"⚠️ Режим не указан, используем первый доступный: {getter_name}")
|
||||||
else:
|
else:
|
||||||
return DataResult(
|
return DataResult(
|
||||||
success=False,
|
success=False,
|
||||||
|
|||||||
@@ -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
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
{"version":"1","format":"xl-single","id":"29118f57-702e-4363-9a41-9f06655e449d","xl":{"version":"3","this":"195a90f4-fc26-46a8-b6d4-0b50b99b1342","sets":[["195a90f4-fc26-46a8-b6d4-0b50b99b1342"]],"distributionAlgo":"SIPMOD+PARITY"}}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,5 +11,4 @@ requests>=2.31.0
|
|||||||
# pytest-cov>=4.0.0
|
# pytest-cov>=4.0.0
|
||||||
# pytest-mock>=3.10.0
|
# pytest-mock>=3.10.0
|
||||||
httpx>=0.24.0
|
httpx>=0.24.0
|
||||||
numpy
|
numpy
|
||||||
streamlit>=1.28.0
|
|
||||||
@@ -1,60 +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("🛑 Для остановки нажмите Ctrl+C")
|
|
||||||
|
|
||||||
# Открываем браузер
|
|
||||||
try:
|
|
||||||
webbrowser.open("http://localhost:8501")
|
|
||||||
print("✅ Браузер открыт")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"⚠️ Не удалось открыть браузер: {e}")
|
|
||||||
|
|
||||||
# Запускаем Streamlit
|
|
||||||
try:
|
|
||||||
subprocess.run([
|
|
||||||
sys.executable, "-m", "streamlit", "run", "app.py",
|
|
||||||
"--server.port", "8501",
|
|
||||||
"--server.address", "localhost",
|
|
||||||
"--server.headless", "false",
|
|
||||||
"--browser.gatherUsageStats", "false"
|
|
||||||
])
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("\n👋 Streamlit остановлен")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
python-3.11.*
|
|
||||||
@@ -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
|
|
||||||
*~
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
FROM python:3.11-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Устанавливаем системные зависимости
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
gcc \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Копируем файлы зависимостей
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
# Устанавливаем 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"]
|
|
||||||
@@ -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**
|
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
import streamlit as st
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
import pandas as pd
|
|
||||||
import io
|
|
||||||
import zipfile
|
|
||||||
from typing import Dict, Any
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Конфигурация страницы
|
|
||||||
st.set_page_config(
|
|
||||||
page_title="NIN Excel Parsers API Demo",
|
|
||||||
page_icon="📊",
|
|
||||||
layout="wide",
|
|
||||||
initial_sidebar_state="expanded"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Конфигурация API - используем переменную окружения или значение по умолчанию
|
|
||||||
API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000")
|
|
||||||
|
|
||||||
def check_api_health():
|
|
||||||
"""Проверка доступности API"""
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{API_BASE_URL}/", timeout=5)
|
|
||||||
return response.status_code == 200
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_available_parsers():
|
|
||||||
"""Получение списка доступных парсеров"""
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{API_BASE_URL}/parsers")
|
|
||||||
if response.status_code == 200:
|
|
||||||
return response.json()["parsers"]
|
|
||||||
return []
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_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:
|
|
||||||
response = requests.get(f"{API_BASE_URL}/server-info")
|
|
||||||
if response.status_code == 200:
|
|
||||||
return response.json()
|
|
||||||
return {}
|
|
||||||
except:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def upload_file_to_api(endpoint: str, file_data: bytes, filename: str):
|
|
||||||
"""Загрузка файла на API"""
|
|
||||||
try:
|
|
||||||
files = {"zip_file": (filename, file_data, "application/zip")}
|
|
||||||
response = requests.post(f"{API_BASE_URL}{endpoint}", files=files)
|
|
||||||
return response.json(), response.status_code
|
|
||||||
except Exception as e:
|
|
||||||
return {"error": str(e)}, 500
|
|
||||||
|
|
||||||
def make_api_request(endpoint: str, data: Dict[str, Any]):
|
|
||||||
"""Выполнение API запроса"""
|
|
||||||
try:
|
|
||||||
response = requests.post(f"{API_BASE_URL}{endpoint}", json=data)
|
|
||||||
return response.json(), response.status_code
|
|
||||||
except Exception as e:
|
|
||||||
return {"error": str(e)}, 500
|
|
||||||
|
|
||||||
def main():
|
|
||||||
st.title("🚀 NIN Excel Parsers API - Демонстрация")
|
|
||||||
st.markdown("---")
|
|
||||||
|
|
||||||
# Проверка доступности API
|
|
||||||
if not check_api_health():
|
|
||||||
st.error(f"❌ API недоступен по адресу {API_BASE_URL}")
|
|
||||||
st.info("Убедитесь, что FastAPI сервер запущен")
|
|
||||||
return
|
|
||||||
|
|
||||||
st.success(f"✅ API доступен по адресу {API_BASE_URL}")
|
|
||||||
|
|
||||||
# Боковая панель с информацией
|
|
||||||
with st.sidebar:
|
|
||||||
st.header("ℹ️ Информация")
|
|
||||||
|
|
||||||
# Информация о сервере
|
|
||||||
server_info = get_server_info()
|
|
||||||
if server_info:
|
|
||||||
st.subheader("Сервер")
|
|
||||||
st.write(f"PID: {server_info.get('process_id', 'N/A')}")
|
|
||||||
st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}")
|
|
||||||
st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB")
|
|
||||||
|
|
||||||
# Доступные парсеры
|
|
||||||
parsers = get_available_parsers()
|
|
||||||
if parsers:
|
|
||||||
st.subheader("Доступные парсеры")
|
|
||||||
for parser in parsers:
|
|
||||||
st.write(f"• {parser}")
|
|
||||||
|
|
||||||
# Основные вкладки - по одной на каждый парсер
|
|
||||||
tab1, tab2, tab3 = st.tabs([
|
|
||||||
"📊 Сводки ПМ",
|
|
||||||
"🏭 Сводки СА",
|
|
||||||
"⛽ Мониторинг топлива"
|
|
||||||
])
|
|
||||||
|
|
||||||
# Вкладка 1: Сводки ПМ - полный функционал
|
|
||||||
with tab1:
|
|
||||||
st.header("📊 Сводки ПМ - Полный функционал")
|
|
||||||
|
|
||||||
# Получаем информацию о геттерах
|
|
||||||
getters_info = get_parser_getters("svodka_pm")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
|
||||||
st.subheader("📤 Загрузка файлов")
|
|
||||||
uploaded_pm = st.file_uploader(
|
|
||||||
"Выберите ZIP архив со сводками ПМ",
|
|
||||||
type=['zip'],
|
|
||||||
key="pm_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
if uploaded_pm is not None:
|
|
||||||
if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"):
|
|
||||||
with st.spinner("Загружаю файл..."):
|
|
||||||
result, status = upload_file_to_api(
|
|
||||||
"/svodka_pm/upload-zip",
|
|
||||||
uploaded_pm.read(),
|
|
||||||
uploaded_pm.name
|
|
||||||
)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success(f"✅ {result.get('message', 'Файл загружен')}")
|
|
||||||
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
|
|
||||||
st.markdown("---")
|
|
||||||
|
|
||||||
# Секция получения данных
|
|
||||||
st.subheader("🔍 Получение данных")
|
|
||||||
|
|
||||||
# Показываем доступные геттеры
|
|
||||||
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:
|
|
||||||
st.subheader("Данные по одному ОГ")
|
|
||||||
|
|
||||||
og_id = st.selectbox(
|
|
||||||
"Выберите ОГ",
|
|
||||||
["SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV",
|
|
||||||
"NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK",
|
|
||||||
"NVNPO", "KLNPZ", "PurNP", "YANOS"],
|
|
||||||
key="pm_single_og"
|
|
||||||
)
|
|
||||||
|
|
||||||
codes = st.multiselect(
|
|
||||||
"Выберите коды строк",
|
|
||||||
[78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
|
|
||||||
default=[78, 79],
|
|
||||||
key="pm_single_codes"
|
|
||||||
)
|
|
||||||
|
|
||||||
columns = st.multiselect(
|
|
||||||
"Выберите столбцы",
|
|
||||||
["БП", "ПП", "СЭБ", "Факт", "План"],
|
|
||||||
default=["БП", "ПП"],
|
|
||||||
key="pm_single_columns"
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.button("🔍 Получить данные по ОГ", key="pm_single_btn"):
|
|
||||||
if codes and columns:
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"getter": "single_og",
|
|
||||||
"id": og_id,
|
|
||||||
"codes": codes,
|
|
||||||
"columns": columns
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_pm/get_data", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
else:
|
|
||||||
st.warning("⚠️ Выберите коды и столбцы")
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("Данные по всем ОГ")
|
|
||||||
|
|
||||||
codes_total = st.multiselect(
|
|
||||||
"Выберите коды строк",
|
|
||||||
[78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
|
|
||||||
default=[78, 79, 394, 395],
|
|
||||||
key="pm_total_codes"
|
|
||||||
)
|
|
||||||
|
|
||||||
columns_total = st.multiselect(
|
|
||||||
"Выберите столбцы",
|
|
||||||
["БП", "ПП", "СЭБ", "Факт", "План"],
|
|
||||||
default=["БП", "ПП", "СЭБ"],
|
|
||||||
key="pm_total_columns"
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"):
|
|
||||||
if codes_total and columns_total:
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"getter": "total_ogs",
|
|
||||||
"codes": codes_total,
|
|
||||||
"columns": columns_total
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_pm/get_data", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
else:
|
|
||||||
st.warning("⚠️ Выберите коды и столбцы")
|
|
||||||
|
|
||||||
# Вкладка 2: Сводки СА - полный функционал
|
|
||||||
with tab2:
|
|
||||||
st.header("🏭 Сводки СА - Полный функционал")
|
|
||||||
|
|
||||||
# Получаем информацию о геттерах
|
|
||||||
getters_info = get_parser_getters("svodka_ca")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
|
||||||
st.subheader("📤 Загрузка файлов")
|
|
||||||
uploaded_ca = st.file_uploader(
|
|
||||||
"Выберите Excel файл сводки СА",
|
|
||||||
type=['xlsx', 'xlsm', 'xls'],
|
|
||||||
key="ca_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
if uploaded_ca is not None:
|
|
||||||
if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"):
|
|
||||||
with st.spinner("Загружаю файл..."):
|
|
||||||
try:
|
|
||||||
files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
|
|
||||||
response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files)
|
|
||||||
result = response.json()
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
st.success(f"✅ {result.get('message', 'Файл загружен')}")
|
|
||||||
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
except Exception as e:
|
|
||||||
st.error(f"❌ Ошибка: {str(e)}")
|
|
||||||
|
|
||||||
st.markdown("---")
|
|
||||||
|
|
||||||
# Секция получения данных
|
|
||||||
st.subheader("🔍 Получение данных")
|
|
||||||
|
|
||||||
# Показываем доступные геттеры
|
|
||||||
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:
|
|
||||||
st.subheader("Параметры запроса")
|
|
||||||
|
|
||||||
modes = st.multiselect(
|
|
||||||
"Выберите режимы",
|
|
||||||
["План", "Факт", "Норматив"],
|
|
||||||
default=["План", "Факт"],
|
|
||||||
key="ca_modes"
|
|
||||||
)
|
|
||||||
|
|
||||||
tables = st.multiselect(
|
|
||||||
"Выберите таблицы",
|
|
||||||
["ТиП", "Топливо", "Потери"],
|
|
||||||
default=["ТиП", "Топливо"],
|
|
||||||
key="ca_tables"
|
|
||||||
)
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("Результат")
|
|
||||||
if st.button("🔍 Получить данные СА", key="ca_btn"):
|
|
||||||
if modes and tables:
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"getter": "get_data",
|
|
||||||
"modes": modes,
|
|
||||||
"tables": tables
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_ca/get_data", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
else:
|
|
||||||
st.warning("⚠️ Выберите режимы и таблицы")
|
|
||||||
|
|
||||||
# Вкладка 3: Мониторинг топлива - полный функционал
|
|
||||||
with tab3:
|
|
||||||
st.header("⛽ Мониторинг топлива - Полный функционал")
|
|
||||||
|
|
||||||
# Получаем информацию о геттерах
|
|
||||||
getters_info = get_parser_getters("monitoring_fuel")
|
|
||||||
|
|
||||||
# Секция загрузки файлов
|
|
||||||
st.subheader("📤 Загрузка файлов")
|
|
||||||
uploaded_fuel = st.file_uploader(
|
|
||||||
"Выберите ZIP архив с мониторингом топлива",
|
|
||||||
type=['zip'],
|
|
||||||
key="fuel_upload"
|
|
||||||
)
|
|
||||||
|
|
||||||
if uploaded_fuel is not None:
|
|
||||||
if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"):
|
|
||||||
with st.spinner("Загружаю файл..."):
|
|
||||||
result, status = upload_file_to_api(
|
|
||||||
"/monitoring_fuel/upload-zip",
|
|
||||||
uploaded_fuel.read(),
|
|
||||||
uploaded_fuel.name
|
|
||||||
)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success(f"✅ {result.get('message', 'Файл загружен')}")
|
|
||||||
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
|
|
||||||
st.markdown("---")
|
|
||||||
|
|
||||||
# Секция получения данных
|
|
||||||
st.subheader("🔍 Получение данных")
|
|
||||||
|
|
||||||
# Показываем доступные геттеры
|
|
||||||
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:
|
|
||||||
st.subheader("Агрегация по колонкам")
|
|
||||||
|
|
||||||
columns_fuel = st.multiselect(
|
|
||||||
"Выберите столбцы",
|
|
||||||
["normativ", "total", "total_1"],
|
|
||||||
default=["normativ", "total"],
|
|
||||||
key="fuel_columns"
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"):
|
|
||||||
if columns_fuel:
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"getter": "total_by_columns",
|
|
||||||
"columns": columns_fuel
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status = make_api_request("/monitoring_fuel/get_data", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
else:
|
|
||||||
st.warning("⚠️ Выберите столбцы")
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
st.subheader("Данные за месяц")
|
|
||||||
|
|
||||||
month = st.selectbox(
|
|
||||||
"Выберите месяц",
|
|
||||||
[f"{i:02d}" for i in range(1, 13)],
|
|
||||||
key="fuel_month"
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
|
|
||||||
with st.spinner("Получаю данные..."):
|
|
||||||
data = {
|
|
||||||
"getter": "month_by_code",
|
|
||||||
"month": month
|
|
||||||
}
|
|
||||||
|
|
||||||
result, status = make_api_request("/monitoring_fuel/get_data", data)
|
|
||||||
|
|
||||||
if status == 200:
|
|
||||||
st.success("✅ Данные получены")
|
|
||||||
st.json(result)
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
|
||||||
|
|
||||||
# Футер
|
|
||||||
st.markdown("---")
|
|
||||||
st.markdown("### 📚 Документация API")
|
|
||||||
st.markdown(f"Полная документация доступна по адресу: {API_BASE_URL}/docs")
|
|
||||||
|
|
||||||
# Информация о проекте
|
|
||||||
with st.expander("ℹ️ О проекте"):
|
|
||||||
st.markdown("""
|
|
||||||
**NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов.
|
|
||||||
|
|
||||||
**Возможности:**
|
|
||||||
- 📊 Парсинг сводок ПМ (план и факт)
|
|
||||||
- 🏭 Парсинг сводок СА
|
|
||||||
- ⛽ Мониторинг топлива
|
|
||||||
|
|
||||||
**Технологии:**
|
|
||||||
- FastAPI
|
|
||||||
- Pandas
|
|
||||||
- MinIO (S3-совместимое хранилище)
|
|
||||||
- Streamlit (веб-интерфейс)
|
|
||||||
""")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
streamlit>=1.28.0
|
|
||||||
requests>=2.31.0
|
|
||||||
pandas>=1.5.0
|
|
||||||
numpy>=1.24.0
|
|
||||||
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()
|
||||||
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"
|
||||||
23
streamlit_app/Dockerfile
Normal file
23
streamlit_app/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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 зависимостей
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Копирование кода приложения
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Открытие порта
|
||||||
|
EXPOSE 8501
|
||||||
|
|
||||||
|
# Запуск Streamlit
|
||||||
|
CMD ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
||||||
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")
|
||||||
7
streamlit_app/requirements.txt
Normal file
7
streamlit_app/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
streamlit>=1.28.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
|
||||||
@@ -16,7 +16,8 @@ st.set_page_config(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Конфигурация API
|
# Конфигурация API
|
||||||
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8000")
|
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():
|
def check_api_health():
|
||||||
"""Проверка доступности API"""
|
"""Проверка доступности API"""
|
||||||
@@ -73,7 +74,7 @@ def main():
|
|||||||
st.info("Убедитесь, что FastAPI сервер запущен")
|
st.info("Убедитесь, что FastAPI сервер запущен")
|
||||||
return
|
return
|
||||||
|
|
||||||
st.success(f"✅ API доступен по адресу {API_BASE_URL}")
|
st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}")
|
||||||
|
|
||||||
# Боковая панель с информацией
|
# Боковая панель с информацией
|
||||||
with st.sidebar:
|
with st.sidebar:
|
||||||
@@ -276,7 +277,7 @@ def main():
|
|||||||
"tables": tables
|
"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:
|
if status == 200:
|
||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
@@ -369,11 +370,39 @@ def main():
|
|||||||
st.json(result)
|
st.json(result)
|
||||||
else:
|
else:
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
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("---")
|
||||||
st.markdown("### 📚 Документация API")
|
st.markdown("### 📚 Документация API")
|
||||||
st.markdown(f"Полная документация доступна по адресу: {API_BASE_URL}/docs")
|
st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs")
|
||||||
|
|
||||||
# Информация о проекте
|
# Информация о проекте
|
||||||
with st.expander("ℹ️ О проекте"):
|
with st.expander("ℹ️ О проекте"):
|
||||||
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