Compare commits
11 Commits
908fb330f4
...
use_schema
| Author | SHA1 | Date | |
|---|---|---|---|
| 79ab91c700 | |||
| b98be22359 | |||
| fc0b4356da | |||
| 72fe115a99 | |||
| 46a30c32ed | |||
| 5e217c7cce | |||
| 7d2747c8fe | |||
| 513ff3c144 | |||
| a0b6e04d99 | |||
| 47a7344755 | |||
| 456e9935f0 |
173
.gitignore
vendored
Normal file
173
.gitignore
vendored
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
__pycache__/
|
||||||
|
python_parser/__pycache__/
|
||||||
|
python_parser/core/__pycache__/
|
||||||
|
python_parser/adapters/__pycache__/
|
||||||
|
python_parser/tests/__pycache__/
|
||||||
|
python_parser/tests/test_core/__pycache__/
|
||||||
|
python_parser/tests/test_adapters/__pycache__/
|
||||||
|
python_parser/tests/test_app/__pycache__/
|
||||||
|
python_parser/app/__pycache__/
|
||||||
|
python_parser/app/schemas/__pycache__/
|
||||||
|
python_parser/app/schemas/test_schemas/__pycache__/
|
||||||
|
python_parser/app/schemas/test_schemas/test_core/__pycache__/
|
||||||
|
python_parser/app/schemas/test_schemas/test_adapters/__pycache__/
|
||||||
|
python_parser/app/schemas/test_schemas/test_app/__pycache__/
|
||||||
|
|
||||||
|
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
log/
|
||||||
|
|
||||||
|
# MinIO data and cache
|
||||||
|
minio_data/
|
||||||
|
.minio.sys/
|
||||||
|
*.meta
|
||||||
|
part.*
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*.orig
|
||||||
|
|
||||||
|
# Data files (Excel, CSV, etc.)
|
||||||
|
*.xlsx
|
||||||
|
*.xls
|
||||||
|
*.xlsm
|
||||||
|
*.csv
|
||||||
|
*.json
|
||||||
|
data/
|
||||||
|
uploads/
|
||||||
|
|
||||||
|
# Cache directories
|
||||||
|
.cache/
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
poetry.lock
|
||||||
|
|
||||||
|
# Celery
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# Local development
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# FastAPI
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Streamlit
|
||||||
|
.streamlit/secrets.toml
|
||||||
|
|
||||||
|
# Node.js (if any frontend components)
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
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"
|
||||||
@@ -28,5 +30,20 @@ services:
|
|||||||
- minio
|
- minio
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
streamlit:
|
||||||
minio_data:
|
build: ./streamlit_app
|
||||||
|
container_name: svodka_streamlit
|
||||||
|
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
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
- fastapi
|
||||||
|
restart: unless-stopped
|
||||||
@@ -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 +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,197 +0,0 @@
|
|||||||
# NIN Excel Parsers API
|
|
||||||
|
|
||||||
API для парсинга Excel отчетов нефтеперерабатывающих заводов (НПЗ) с использованием FastAPI и MinIO для хранения данных.
|
|
||||||
|
|
||||||
## 🚀 Быстрый запуск
|
|
||||||
|
|
||||||
### **Вариант 1: Только MinIO в Docker + FastAPI локально**
|
|
||||||
```bash
|
|
||||||
# Запуск MinIO в Docker
|
|
||||||
docker-compose up -d minio
|
|
||||||
|
|
||||||
# Запуск FastAPI локально
|
|
||||||
python run_dev.py
|
|
||||||
|
|
||||||
# В отдельном терминале запуск Streamlit
|
|
||||||
python run_streamlit.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Вариант 2: MinIO + FastAPI в Docker + Streamlit локально**
|
|
||||||
```bash
|
|
||||||
# Запуск MinIO и FastAPI в Docker
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# В отдельном терминале запуск Streamlit
|
|
||||||
python run_streamlit.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Вариант 3: Только MinIO в Docker**
|
|
||||||
```bash
|
|
||||||
# Запуск только MinIO
|
|
||||||
docker-compose up -d minio
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Описание сервисов
|
|
||||||
|
|
||||||
- **MinIO** (порт 9000-9001): S3-совместимое хранилище для данных
|
|
||||||
- **FastAPI** (порт 8000): API сервер для парсинга Excel файлов
|
|
||||||
- **Streamlit** (порт 8501): Веб-интерфейс для демонстрации API
|
|
||||||
|
|
||||||
## 🔧 Диагностика
|
|
||||||
|
|
||||||
Для проверки состояния всех сервисов:
|
|
||||||
```bash
|
|
||||||
python check_services.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛑 Остановка
|
|
||||||
|
|
||||||
### Остановка Docker сервисов:
|
|
||||||
```bash
|
|
||||||
# Все сервисы
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Только MinIO
|
|
||||||
docker-compose stop minio
|
|
||||||
```
|
|
||||||
|
|
||||||
### Остановка Streamlit:
|
|
||||||
```bash
|
|
||||||
# Нажмите Ctrl+C в терминале с Streamlit
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Структура проекта
|
|
||||||
|
|
||||||
```
|
|
||||||
python_parser/
|
|
||||||
├── app/ # FastAPI приложение
|
|
||||||
│ ├── main.py # Основной файл приложения
|
|
||||||
│ └── schemas/ # Pydantic схемы
|
|
||||||
├── core/ # Бизнес-логика
|
|
||||||
│ ├── models.py # Модели данных
|
|
||||||
│ ├── ports.py # Интерфейсы (порты)
|
|
||||||
│ └── services.py # Сервисы
|
|
||||||
├── adapters/ # Адаптеры для внешних систем
|
|
||||||
│ ├── storage.py # MinIO адаптер
|
|
||||||
│ └── parsers/ # Парсеры Excel файлов
|
|
||||||
├── data/ # Тестовые данные
|
|
||||||
├── docker-compose.yml # Docker Compose конфигурация
|
|
||||||
├── Dockerfile # Docker образ для FastAPI
|
|
||||||
├── run_dev.py # Запуск FastAPI локально
|
|
||||||
├── run_streamlit.py # Запуск Streamlit
|
|
||||||
└── check_services.py # Диагностика сервисов
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Доступные эндпоинты
|
|
||||||
|
|
||||||
- **GET /** - Информация об API
|
|
||||||
- **GET /docs** - Swagger документация
|
|
||||||
- **POST /svodka_pm/upload-zip** - Загрузка сводок ПМ
|
|
||||||
- **POST /svodka_ca/upload-zip** - Загрузка сводок ЦА
|
|
||||||
- **POST /monitoring_fuel/upload-zip** - Загрузка мониторинга топлива
|
|
||||||
- **GET /svodka_pm/data** - Получение данных сводок ПМ
|
|
||||||
- **GET /svodka_ca/data** - Получение данных сводок ЦА
|
|
||||||
- **GET /monitoring_fuel/data** - Получение данных мониторинга топлива
|
|
||||||
|
|
||||||
## 📊 Поддерживаемые типы отчетов
|
|
||||||
|
|
||||||
1. **svodka_pm** - Сводки по переработке нефти (ПМ)
|
|
||||||
2. **svodka_ca** - Сводки по переработке нефти (ЦА)
|
|
||||||
3. **monitoring_fuel** - Мониторинг топлива
|
|
||||||
|
|
||||||
## 🐳 Docker команды
|
|
||||||
|
|
||||||
### Сборка и запуск:
|
|
||||||
```bash
|
|
||||||
# Все сервисы
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Только MinIO
|
|
||||||
docker-compose up -d minio
|
|
||||||
|
|
||||||
# Только FastAPI (требует MinIO)
|
|
||||||
docker-compose up -d fastapi
|
|
||||||
```
|
|
||||||
|
|
||||||
### Просмотр логов:
|
|
||||||
```bash
|
|
||||||
# Все сервисы
|
|
||||||
docker-compose logs
|
|
||||||
|
|
||||||
# Конкретный сервис
|
|
||||||
docker-compose logs fastapi
|
|
||||||
docker-compose logs minio
|
|
||||||
```
|
|
||||||
|
|
||||||
### Остановка:
|
|
||||||
```bash
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Устранение неполадок
|
|
||||||
|
|
||||||
### Проблема: "Streamlit не может подключиться к FastAPI"
|
|
||||||
|
|
||||||
**Симптомы:**
|
|
||||||
- Streamlit открывается, но показывает "API недоступен по адресу http://localhost:8000"
|
|
||||||
- FastAPI не отвечает на порту 8000
|
|
||||||
|
|
||||||
**Решения:**
|
|
||||||
|
|
||||||
1. **Проверьте порты:**
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
netstat -an | findstr :8000
|
|
||||||
|
|
||||||
# Linux/Mac
|
|
||||||
netstat -an | grep :8000
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Перезапустите FastAPI:**
|
|
||||||
```bash
|
|
||||||
# Остановите текущий процесс (Ctrl+C)
|
|
||||||
python run_dev.py
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Проверьте логи Docker:**
|
|
||||||
```bash
|
|
||||||
docker-compose logs fastapi
|
|
||||||
```
|
|
||||||
|
|
||||||
### Проблема: "MinIO недоступен"
|
|
||||||
|
|
||||||
**Решения:**
|
|
||||||
1. Запустите Docker Desktop
|
|
||||||
2. Проверьте статус контейнера: `docker ps`
|
|
||||||
3. Перезапустите MinIO: `docker-compose restart minio`
|
|
||||||
|
|
||||||
### Проблема: "Порт уже занят"
|
|
||||||
|
|
||||||
**Решения:**
|
|
||||||
1. Найдите процесс: `netstat -ano | findstr :8000`
|
|
||||||
2. Остановите процесс: `taskkill /PID <номер_процесса>`
|
|
||||||
3. Или используйте другой порт в конфигурации
|
|
||||||
|
|
||||||
## 🚀 Разработка
|
|
||||||
|
|
||||||
### Добавление нового парсера:
|
|
||||||
|
|
||||||
1. Создайте файл в `adapters/parsers/`
|
|
||||||
2. Реализуйте интерфейс `ParserPort`
|
|
||||||
3. Добавьте в `core/services.py`
|
|
||||||
4. Создайте схемы в `app/schemas/`
|
|
||||||
5. Добавьте эндпоинты в `app/main.py`
|
|
||||||
|
|
||||||
### Тестирование:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Запуск тестов
|
|
||||||
pytest
|
|
||||||
|
|
||||||
# Запуск с покрытием
|
|
||||||
pytest --cov=.
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Лицензия
|
|
||||||
|
|
||||||
Проект разработан для внутреннего использования НИН.
|
|
||||||
@@ -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.
@@ -1,9 +1,11 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import re
|
import re
|
||||||
from typing import Dict
|
import zipfile
|
||||||
|
from typing import Dict, Tuple
|
||||||
from core.ports import ParserPort
|
from core.ports import ParserPort
|
||||||
from adapters.pconfig import data_to_json, get_object_by_name
|
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
||||||
|
from app.schemas.monitoring_fuel import MonitoringFuelTotalRequest, MonitoringFuelMonthRequest
|
||||||
|
from adapters.pconfig import data_to_json
|
||||||
|
|
||||||
|
|
||||||
class MonitoringFuelParser(ParserPort):
|
class MonitoringFuelParser(ParserPort):
|
||||||
@@ -11,71 +13,58 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
|
|
||||||
name = "Мониторинг топлива"
|
name = "Мониторинг топлива"
|
||||||
|
|
||||||
def find_header_row(self, file_path: str, sheet: str, search_value: str = "Установка", max_rows: int = 50) -> int:
|
def _register_default_getters(self):
|
||||||
"""Определение индекса заголовка в Excel по ключевому слову"""
|
"""Регистрация геттеров по умолчанию"""
|
||||||
# Читаем первые max_rows строк без заголовков
|
# Используем схемы Pydantic как единый источник правды
|
||||||
df_temp = pd.read_excel(
|
register_getter_from_schema(
|
||||||
file_path,
|
parser_instance=self,
|
||||||
sheet_name=sheet,
|
getter_name="total_by_columns",
|
||||||
header=None,
|
method=self._get_total_by_columns,
|
||||||
nrows=max_rows
|
schema_class=MonitoringFuelTotalRequest,
|
||||||
|
description="Агрегация данных по колонкам"
|
||||||
|
)
|
||||||
|
|
||||||
|
register_getter_from_schema(
|
||||||
|
parser_instance=self,
|
||||||
|
getter_name="month_by_code",
|
||||||
|
method=self._get_month_by_code,
|
||||||
|
schema_class=MonitoringFuelMonthRequest,
|
||||||
|
description="Получение данных за конкретный месяц"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
def _get_total_by_columns(self, params: dict):
|
||||||
for idx, row in df_temp.iterrows():
|
"""Агрегация данных по колонкам"""
|
||||||
if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
|
validated_params = validate_params_with_schema(params, MonitoringFuelTotalRequest)
|
||||||
return idx + 1 # возвращаем индекс строки (0-based)
|
|
||||||
|
columns = validated_params["columns"]
|
||||||
|
|
||||||
|
# TODO: Переделать под новую архитектуру
|
||||||
|
df_means, _ = self.aggregate_by_columns(self.df, columns)
|
||||||
|
return df_means.to_dict(orient='index')
|
||||||
|
|
||||||
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
def _get_month_by_code(self, params: dict):
|
||||||
|
"""Получение данных за конкретный месяц"""
|
||||||
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
|
validated_params = validate_params_with_schema(params, MonitoringFuelMonthRequest)
|
||||||
|
|
||||||
|
month = validated_params["month"]
|
||||||
|
|
||||||
|
# TODO: Переделать под новую архитектуру
|
||||||
|
df_month = self.get_month(self.df, month)
|
||||||
|
return df_month.to_dict(orient='index')
|
||||||
|
|
||||||
def parse_single(self, file, sheet, header_num=None):
|
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||||
''' Собственно парсер отчетов одного объекта'''
|
"""Парсинг файла и возврат DataFrame"""
|
||||||
# Автоопределение header_num, если не передан
|
# Сохраняем DataFrame для использования в геттерах
|
||||||
if header_num is None:
|
self.df = self.parse_monitoring_fuel_files(file_path, params)
|
||||||
header_num = self.find_header_row(file, sheet, search_value="Установка")
|
return self.df
|
||||||
# Читаем весь лист, начиная с найденной строки как заголовок
|
|
||||||
df_full = pd.read_excel(
|
|
||||||
file,
|
|
||||||
sheet_name=sheet,
|
|
||||||
header=header_num,
|
|
||||||
usecols=None,
|
|
||||||
index_col=None
|
|
||||||
)
|
|
||||||
|
|
||||||
# === Удаление полностью пустых столбцов ===
|
def parse_monitoring_fuel_files(self, zip_path: str, params: dict) -> Dict[str, pd.DataFrame]:
|
||||||
df_clean = df_full.replace(r'^\s*$', pd.NA, regex=True) # заменяем пустые строки на NA
|
"""Парсинг ZIP архива с файлами мониторинга топлива"""
|
||||||
df_clean = df_clean.dropna(axis=1, how='all') # удаляем столбцы, где все значения — NA
|
|
||||||
df_full = df_full.loc[:, df_clean.columns] # оставляем только непустые столбцы
|
|
||||||
|
|
||||||
# === Переименовываем нужные столбцы по позициям ===
|
|
||||||
if len(df_full.columns) < 2:
|
|
||||||
raise ValueError("DataFrame должен содержать как минимум 2 столбца.")
|
|
||||||
|
|
||||||
new_columns = df_full.columns.tolist()
|
|
||||||
|
|
||||||
new_columns[0] = 'name'
|
|
||||||
new_columns[1] = 'normativ'
|
|
||||||
new_columns[-2] = 'total'
|
|
||||||
new_columns[-1] = 'total_1'
|
|
||||||
|
|
||||||
df_full.columns = new_columns
|
|
||||||
|
|
||||||
# Проверяем, что колонка 'name' существует
|
|
||||||
if 'name' in df_full.columns:
|
|
||||||
# Применяем функцию get_id_by_name к каждой строке в колонке 'name'
|
|
||||||
df_full['id'] = df_full['name'].apply(get_object_by_name)
|
|
||||||
|
|
||||||
# Устанавливаем id как индекс
|
|
||||||
df_full.set_index('id', inplace=True)
|
|
||||||
print(f"Окончательное количество столбцов: {len(df_full.columns)}")
|
|
||||||
return df_full
|
|
||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> dict:
|
|
||||||
import zipfile
|
|
||||||
df_monitorings = {} # ЭТО СЛОВАРЬ ДАТАФРЕЙМОВ, ГДЕ КЛЮЧ - НОМЕР МЕСЯЦА, ЗНАЧЕНИЕ - ДАТАФРЕЙМ
|
df_monitorings = {} # ЭТО СЛОВАРЬ ДАТАФРЕЙМОВ, ГДЕ КЛЮЧ - НОМЕР МЕСЯЦА, ЗНАЧЕНИЕ - ДАТАФРЕЙМ
|
||||||
|
|
||||||
with zipfile.ZipFile(file_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 month in range(1, 13):
|
for month in range(1, 13):
|
||||||
@@ -103,7 +92,70 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
|
|
||||||
return df_monitorings
|
return df_monitorings
|
||||||
|
|
||||||
def aggregate_by_columns(self, df_dict: Dict[str, pd.DataFrame], columns):
|
def find_header_row(self, file_path: str, sheet: str, search_value: str = "Установка", max_rows: int = 50) -> int:
|
||||||
|
"""Определение индекса заголовка в Excel по ключевому слову"""
|
||||||
|
# Читаем первые max_rows строк без заголовков
|
||||||
|
df_temp = pd.read_excel(
|
||||||
|
file_path,
|
||||||
|
sheet_name=sheet,
|
||||||
|
header=None,
|
||||||
|
nrows=max_rows,
|
||||||
|
engine='openpyxl'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
||||||
|
for idx, row in df_temp.iterrows():
|
||||||
|
if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
|
||||||
|
print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
|
||||||
|
return idx + 1 # возвращаем индекс строки (0-based)
|
||||||
|
|
||||||
|
raise ValueError(f"Не найдена строка с заголовком '{search_value}' в первых {max_rows} строках.")
|
||||||
|
|
||||||
|
def parse_single(self, file, sheet, header_num=None):
|
||||||
|
''' Собственно парсер отчетов одного объекта'''
|
||||||
|
# Автоопределение header_num, если не передан
|
||||||
|
if header_num is None:
|
||||||
|
header_num = self.find_header_row(file, sheet, search_value="Установка")
|
||||||
|
# Читаем весь лист, начиная с найденной строки как заголовок
|
||||||
|
df_full = pd.read_excel(
|
||||||
|
file,
|
||||||
|
sheet_name=sheet,
|
||||||
|
header=header_num,
|
||||||
|
usecols=None,
|
||||||
|
index_col=None,
|
||||||
|
engine='openpyxl'
|
||||||
|
)
|
||||||
|
|
||||||
|
# === Удаление полностью пустых столбцов ===
|
||||||
|
df_clean = df_full.replace(r'^\s*$', pd.NA, regex=True) # заменяем пустые строки на NA
|
||||||
|
df_clean = df_clean.dropna(axis=1, how='all') # удаляем столбцы, где все значения — NA
|
||||||
|
df_full = df_full.loc[:, df_clean.columns] # оставляем только непустые столбцы
|
||||||
|
|
||||||
|
# === Переименовываем нужные столбцы по позициям ===
|
||||||
|
if len(df_full.columns) < 2:
|
||||||
|
raise ValueError("DataFrame должен содержать как минимум 2 столбца.")
|
||||||
|
|
||||||
|
new_columns = df_full.columns.tolist()
|
||||||
|
|
||||||
|
new_columns[0] = 'name'
|
||||||
|
new_columns[1] = 'normativ'
|
||||||
|
new_columns[-2] = 'total'
|
||||||
|
new_columns[-1] = 'total_1'
|
||||||
|
|
||||||
|
df_full.columns = new_columns
|
||||||
|
|
||||||
|
# Проверяем, что колонка 'name' существует
|
||||||
|
if 'name' in df_full.columns:
|
||||||
|
# Применяем функцию get_id_by_name к каждой строке в колонке 'name'
|
||||||
|
# df_full['id'] = df_full['name'].apply(get_object_by_name) # This line was removed as per new_code
|
||||||
|
pass # Placeholder for new_code
|
||||||
|
|
||||||
|
# Устанавливаем id как индекс
|
||||||
|
df_full.set_index('id', inplace=True)
|
||||||
|
print(f"Окончательное количество столбцов: {len(df_full.columns)}")
|
||||||
|
return df_full
|
||||||
|
|
||||||
|
def aggregate_by_columns(self, df_dict: Dict[str, pd.DataFrame], columns: list) -> Tuple[pd.DataFrame, Dict[str, pd.DataFrame]]:
|
||||||
''' Служебная функция. Агрегация данных по среднему по определенным колонкам. '''
|
''' Служебная функция. Агрегация данных по среднему по определенным колонкам. '''
|
||||||
all_data = {} # Для хранения полных данных (месяцы) по каждой колонке
|
all_data = {} # Для хранения полных данных (месяцы) по каждой колонке
|
||||||
means = {} # Для хранения средних
|
means = {} # Для хранения средних
|
||||||
@@ -185,22 +237,3 @@ class MonitoringFuelParser(ParserPort):
|
|||||||
total.name = 'mean'
|
total.name = 'mean'
|
||||||
|
|
||||||
return total, df_combined
|
return total, df_combined
|
||||||
|
|
||||||
def get_value(self, df, params):
|
|
||||||
mode = params.get("mode", "total")
|
|
||||||
columns = params.get("columns", None)
|
|
||||||
month = params.get("month", None)
|
|
||||||
data = None
|
|
||||||
if mode == "total":
|
|
||||||
if not columns:
|
|
||||||
raise ValueError("Отсутствуют идентификаторы столбцов")
|
|
||||||
df_means, _ = self.aggregate_by_columns(df, columns)
|
|
||||||
data = df_means.to_dict(orient='index')
|
|
||||||
elif mode == "month":
|
|
||||||
if not month:
|
|
||||||
raise ValueError("Отсутствуют идентификатор месяца")
|
|
||||||
df_month = self.get_month(df, month)
|
|
||||||
data = df_month.to_dict(orient='index')
|
|
||||||
|
|
||||||
json_result = data_to_json(data)
|
|
||||||
return json_result
|
|
||||||
|
|||||||
@@ -2,89 +2,53 @@ 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
|
||||||
|
|
||||||
|
|
||||||
class SvodkaCAParser(ParserPort):
|
class SvodkaCAParser(ParserPort):
|
||||||
"""Парсер для сводки СА"""
|
"""Парсер для сводок СА"""
|
||||||
|
|
||||||
name = "Сводка СА"
|
name = "Сводки СА"
|
||||||
|
|
||||||
def extract_all_tables(self, file_path, sheet_name=0):
|
def _register_default_getters(self):
|
||||||
"""Извлекает все таблицы из Excel файла"""
|
"""Регистрация геттеров по умолчанию"""
|
||||||
df = pd.read_excel(file_path, sheet_name=sheet_name, header=None)
|
# Используем схемы Pydantic как единый источник правды
|
||||||
df_filled = df.fillna('')
|
register_getter_from_schema(
|
||||||
df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True)
|
parser_instance=self,
|
||||||
|
getter_name="get_data",
|
||||||
|
method=self._get_data_wrapper,
|
||||||
|
schema_class=SvodkaCARequest,
|
||||||
|
description="Получение данных по режимам и таблицам"
|
||||||
|
)
|
||||||
|
|
||||||
non_empty_rows = ~(df_clean.eq('').all(axis=1))
|
def _get_data_wrapper(self, params: dict):
|
||||||
non_empty_cols = ~(df_clean.eq('').all(axis=0))
|
"""Получение данных по режимам и таблицам"""
|
||||||
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
|
validated_params = validate_params_with_schema(params, SvodkaCARequest)
|
||||||
|
|
||||||
|
modes = validated_params["modes"]
|
||||||
|
tables = validated_params["tables"]
|
||||||
|
|
||||||
|
# TODO: Переделать под новую архитектуру
|
||||||
|
data_dict = {}
|
||||||
|
for mode in modes:
|
||||||
|
data_dict[mode] = self.get_data(self.df, mode, tables)
|
||||||
|
return self.data_dict_to_json(data_dict)
|
||||||
|
|
||||||
row_indices = non_empty_rows[non_empty_rows].index.tolist()
|
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||||
col_indices = non_empty_cols[non_empty_cols].index.tolist()
|
"""Парсинг файла и возврат DataFrame"""
|
||||||
|
# Сохраняем DataFrame для использования в геттерах
|
||||||
|
self.df = self.parse_svodka_ca(file_path, params)
|
||||||
|
return self.df
|
||||||
|
|
||||||
if not row_indices or not col_indices:
|
def parse_svodka_ca(self, file_path: str, params: dict) -> dict:
|
||||||
return []
|
"""Парсинг сводки СА"""
|
||||||
|
# Получаем параметры из params
|
||||||
row_blocks = self._get_contiguous_blocks(row_indices)
|
sheet_name = params.get('sheet_name', 0) # По умолчанию первый лист
|
||||||
col_blocks = self._get_contiguous_blocks(col_indices)
|
inclusion_list = params.get('inclusion_list', {'ТиП', 'Топливо', 'Потери'})
|
||||||
|
|
||||||
tables = []
|
|
||||||
for r_start, r_end in row_blocks:
|
|
||||||
for c_start, c_end in col_blocks:
|
|
||||||
block = df.iloc[r_start:r_end + 1, c_start:c_end + 1]
|
|
||||||
if block.empty or block.fillna('').astype(str).replace(r'^\s*$', '', regex=True).eq('').all().all():
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self._is_header_row(block.iloc[0]):
|
|
||||||
block.columns = block.iloc[0]
|
|
||||||
block = block.iloc[1:].reset_index(drop=True)
|
|
||||||
else:
|
|
||||||
block = block.reset_index(drop=True)
|
|
||||||
block.columns = [f"col_{i}" for i in range(block.shape[1])]
|
|
||||||
|
|
||||||
tables.append(block)
|
|
||||||
|
|
||||||
return tables
|
|
||||||
|
|
||||||
def _get_contiguous_blocks(self, indices):
|
|
||||||
"""Группирует индексы в непрерывные блоки"""
|
|
||||||
if not indices:
|
|
||||||
return []
|
|
||||||
blocks = []
|
|
||||||
start = indices[0]
|
|
||||||
for i in range(1, len(indices)):
|
|
||||||
if indices[i] != indices[i-1] + 1:
|
|
||||||
blocks.append((start, indices[i-1]))
|
|
||||||
start = indices[i]
|
|
||||||
blocks.append((start, indices[-1]))
|
|
||||||
return blocks
|
|
||||||
|
|
||||||
def _is_header_row(self, series):
|
|
||||||
"""Определяет, похожа ли строка на заголовок"""
|
|
||||||
series_str = series.astype(str).str.strip()
|
|
||||||
non_empty = series_str[series_str != '']
|
|
||||||
if len(non_empty) == 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_not_numeric(val):
|
|
||||||
try:
|
|
||||||
float(val.replace(',', '.'))
|
|
||||||
return False
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return True
|
|
||||||
|
|
||||||
not_numeric_count = non_empty.apply(is_not_numeric).sum()
|
|
||||||
return not_numeric_count / len(non_empty) > 0.6
|
|
||||||
|
|
||||||
def _get_og_by_name(self, name):
|
|
||||||
"""Функция для получения ID по имени (упрощенная версия)"""
|
|
||||||
# Упрощенная версия - возвращаем имя как есть
|
|
||||||
if not name or not isinstance(name, str):
|
|
||||||
return None
|
|
||||||
return name.strip()
|
|
||||||
|
|
||||||
def parse_sheet(self, file_path, sheet_name, inclusion_list):
|
|
||||||
"""Собственно функция парсинга отчета СА"""
|
|
||||||
# === Извлечение и фильтрация ===
|
# === Извлечение и фильтрация ===
|
||||||
tables = self.extract_all_tables(file_path, sheet_name)
|
tables = self.extract_all_tables(file_path, sheet_name)
|
||||||
|
|
||||||
@@ -190,76 +154,185 @@ class SvodkaCAParser(ParserPort):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> dict:
|
def extract_all_tables(self, file_path, sheet_name=0):
|
||||||
"""Парсинг файла сводки СА"""
|
"""Извлечение всех таблиц из Excel файла"""
|
||||||
# === Точка входа. Нужно выгрузить три таблицы: План, Факт и Норматив ===
|
df = pd.read_excel(file_path, sheet_name=sheet_name, header=None, engine='openpyxl')
|
||||||
# Выгружаем План в df_ca_plan
|
df_filled = df.fillna('')
|
||||||
inclusion_list_plan = {
|
df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True)
|
||||||
"ТиП, %",
|
|
||||||
"Топливо итого, тонн",
|
|
||||||
"Топливо итого, %",
|
|
||||||
"Топливо на технологию, тонн",
|
|
||||||
"Топливо на технологию, %",
|
|
||||||
"Топливо на энергетику, тонн",
|
|
||||||
"Топливо на энергетику, %",
|
|
||||||
"Потери итого, тонн",
|
|
||||||
"Потери итого, %",
|
|
||||||
"в т.ч. Идентифицированные безвозвратные потери, тонн**",
|
|
||||||
"в т.ч. Идентифицированные безвозвратные потери, %**",
|
|
||||||
"в т.ч. Неидентифицированные потери, тонн**",
|
|
||||||
"в т.ч. Неидентифицированные потери, %**"
|
|
||||||
}
|
|
||||||
|
|
||||||
df_ca_plan = self.parse_sheet(file_path, 'План', inclusion_list_plan) # ЭТО ДАТАФРЕЙМ ПЛАНА В СВОДКЕ ЦА
|
non_empty_rows = ~(df_clean.eq('').all(axis=1))
|
||||||
print(f"\n--- Объединённый и отсортированный План: {df_ca_plan.shape} ---")
|
non_empty_cols = ~(df_clean.eq('').all(axis=0))
|
||||||
|
|
||||||
# Выгружаем Факт
|
row_indices = non_empty_rows[non_empty_rows].index.tolist()
|
||||||
inclusion_list_fact = {
|
col_indices = non_empty_cols[non_empty_cols].index.tolist()
|
||||||
"ТиП, %",
|
|
||||||
"Топливо итого, тонн",
|
|
||||||
"Топливо итого, %",
|
|
||||||
"Топливо на технологию, тонн",
|
|
||||||
"Топливо на технологию, %",
|
|
||||||
"Топливо на энергетику, тонн",
|
|
||||||
"Топливо на энергетику, %",
|
|
||||||
"Потери итого, тонн",
|
|
||||||
"Потери итого, %",
|
|
||||||
"в т.ч. Идентифицированные безвозвратные потери, тонн",
|
|
||||||
"в т.ч. Идентифицированные безвозвратные потери, %",
|
|
||||||
"в т.ч. Неидентифицированные потери, тонн",
|
|
||||||
"в т.ч. Неидентифицированные потери, %"
|
|
||||||
}
|
|
||||||
|
|
||||||
df_ca_fact = self.parse_sheet(file_path, 'Факт', inclusion_list_fact) # ЭТО ДАТАФРЕЙМ ФАКТА В СВОДКЕ ЦА
|
if not row_indices or not col_indices:
|
||||||
print(f"\n--- Объединённый и отсортированный Факт: {df_ca_fact.shape} ---")
|
return []
|
||||||
|
|
||||||
# Выгружаем План в df_ca_normativ
|
row_blocks = self._get_contiguous_blocks(row_indices)
|
||||||
inclusion_list_normativ = {
|
col_blocks = self._get_contiguous_blocks(col_indices)
|
||||||
"Топливо итого, тонн",
|
|
||||||
"Топливо итого, %",
|
|
||||||
"Топливо на технологию, тонн",
|
|
||||||
"Топливо на технологию, %",
|
|
||||||
"Топливо на энергетику, тонн",
|
|
||||||
"Топливо на энергетику, %",
|
|
||||||
"Потери итого, тонн",
|
|
||||||
"Потери итого, %",
|
|
||||||
"в т.ч. Идентифицированные безвозвратные потери, тонн**",
|
|
||||||
"в т.ч. Идентифицированные безвозвратные потери, %**",
|
|
||||||
"в т.ч. Неидентифицированные потери, тонн**",
|
|
||||||
"в т.ч. Неидентифицированные потери, %**"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ЭТО ДАТАФРЕЙМ НОРМАТИВА В СВОДКЕ ЦА
|
tables = []
|
||||||
df_ca_normativ = self.parse_sheet(file_path, 'Норматив', inclusion_list_normativ)
|
for r_start, r_end in row_blocks:
|
||||||
|
for c_start, c_end in col_blocks:
|
||||||
|
block = df.iloc[r_start:r_end + 1, c_start:c_end + 1]
|
||||||
|
if block.empty or block.fillna('').astype(str).replace(r'^\s*$', '', regex=True).eq('').all().all():
|
||||||
|
continue
|
||||||
|
|
||||||
print(f"\n--- Объединённый и отсортированный Норматив: {df_ca_normativ.shape} ---")
|
if self._is_header_row(block.iloc[0]):
|
||||||
|
block.columns = block.iloc[0]
|
||||||
|
block = block.iloc[1:].reset_index(drop=True)
|
||||||
|
else:
|
||||||
|
block = block.reset_index(drop=True)
|
||||||
|
block.columns = [f"col_{i}" for i in range(block.shape[1])]
|
||||||
|
|
||||||
df_dict = {
|
tables.append(block)
|
||||||
"plan": df_ca_plan,
|
|
||||||
"fact": df_ca_fact,
|
return tables
|
||||||
"normativ": df_ca_normativ
|
|
||||||
}
|
def _get_contiguous_blocks(self, indices):
|
||||||
return df_dict
|
"""Группирует индексы в непрерывные блоки"""
|
||||||
|
if not indices:
|
||||||
|
return []
|
||||||
|
blocks = []
|
||||||
|
start = indices[0]
|
||||||
|
for i in range(1, len(indices)):
|
||||||
|
if indices[i] != indices[i-1] + 1:
|
||||||
|
blocks.append((start, indices[i-1]))
|
||||||
|
start = indices[i]
|
||||||
|
blocks.append((start, indices[-1]))
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
def _is_header_row(self, series):
|
||||||
|
"""Определяет, похожа ли строка на заголовок"""
|
||||||
|
series_str = series.astype(str).str.strip()
|
||||||
|
non_empty = series_str[series_str != '']
|
||||||
|
if len(non_empty) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_not_numeric(val):
|
||||||
|
try:
|
||||||
|
float(val.replace(',', '.'))
|
||||||
|
return False
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return True
|
||||||
|
|
||||||
|
not_numeric_count = non_empty.apply(is_not_numeric).sum()
|
||||||
|
return not_numeric_count / len(non_empty) > 0.6
|
||||||
|
|
||||||
|
def _get_og_by_name(self, name):
|
||||||
|
"""Функция для получения ID по имени (упрощенная версия)"""
|
||||||
|
# Упрощенная версия - возвращаем имя как есть
|
||||||
|
if not name or not isinstance(name, str):
|
||||||
|
return None
|
||||||
|
return name.strip()
|
||||||
|
|
||||||
|
def parse_sheet(self, file_path: str, sheet_name: str, inclusion_list: set) -> pd.DataFrame:
|
||||||
|
"""Парсинг листа Excel"""
|
||||||
|
# === Извлечение и фильтрация ===
|
||||||
|
tables = self.extract_all_tables(file_path, sheet_name)
|
||||||
|
|
||||||
|
# Фильтруем таблицы: оставляем только те, где первая строка содержит нужные заголовки
|
||||||
|
filtered_tables = []
|
||||||
|
for table in tables:
|
||||||
|
if table.empty:
|
||||||
|
continue
|
||||||
|
first_row_values = table.iloc[0].astype(str).str.strip().tolist()
|
||||||
|
if any(val in inclusion_list for val in first_row_values):
|
||||||
|
filtered_tables.append(table)
|
||||||
|
|
||||||
|
tables = filtered_tables
|
||||||
|
|
||||||
|
# === Итоговый список таблиц датафреймов ===
|
||||||
|
result_list = []
|
||||||
|
|
||||||
|
for table in tables:
|
||||||
|
if table.empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Получаем первую строку (до удаления)
|
||||||
|
first_row_values = table.iloc[0].astype(str).str.strip().tolist()
|
||||||
|
|
||||||
|
# Находим, какой элемент из inclusion_list присутствует
|
||||||
|
matched_key = None
|
||||||
|
for val in first_row_values:
|
||||||
|
if val in inclusion_list:
|
||||||
|
matched_key = val
|
||||||
|
break # берём первый совпадающий заголовок
|
||||||
|
|
||||||
|
if matched_key is None:
|
||||||
|
continue # на всякий случай (хотя уже отфильтровано)
|
||||||
|
|
||||||
|
# Удаляем первую строку (заголовок) и сбрасываем индекс
|
||||||
|
df_cleaned = table.iloc[1:].copy().reset_index(drop=True)
|
||||||
|
|
||||||
|
# Пропускаем, если таблица пустая
|
||||||
|
if df_cleaned.empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Первая строка становится заголовком
|
||||||
|
new_header = df_cleaned.iloc[0] # извлекаем первую строку как потенциальные названия столбцов
|
||||||
|
|
||||||
|
# Преобразуем заголовок: только первый столбец может быть заменён на "name"
|
||||||
|
cleaned_header = []
|
||||||
|
|
||||||
|
# Обрабатываем первый столбец отдельно
|
||||||
|
first_item = new_header.iloc[0] if isinstance(new_header, pd.Series) else new_header[0]
|
||||||
|
first_item_str = str(first_item).strip() if pd.notna(first_item) else ""
|
||||||
|
if first_item_str == "" or first_item_str == "nan":
|
||||||
|
cleaned_header.append("name")
|
||||||
|
else:
|
||||||
|
cleaned_header.append(first_item_str)
|
||||||
|
|
||||||
|
# Остальные столбцы добавляем без изменений (или с минимальной очисткой)
|
||||||
|
for item in new_header[1:]:
|
||||||
|
# Опционально: приводим к строке и убираем лишние пробелы, но не заменяем на "name"
|
||||||
|
item_str = str(item).strip() if pd.notna(item) else ""
|
||||||
|
cleaned_header.append(item_str)
|
||||||
|
|
||||||
|
# Применяем очищенные названия столбцов
|
||||||
|
df_cleaned = df_cleaned[1:] # удаляем строку с заголовком
|
||||||
|
df_cleaned.columns = cleaned_header
|
||||||
|
df_cleaned = df_cleaned.reset_index(drop=True)
|
||||||
|
|
||||||
|
if matched_key.endswith('**'):
|
||||||
|
cleaned_key = matched_key[:-2] # удаляем последние **
|
||||||
|
else:
|
||||||
|
cleaned_key = matched_key
|
||||||
|
|
||||||
|
# Добавляем новую колонку с именем параметра
|
||||||
|
df_cleaned["table"] = cleaned_key
|
||||||
|
|
||||||
|
# Проверяем, что колонка 'name' существует
|
||||||
|
if 'name' not in df_cleaned.columns:
|
||||||
|
print(
|
||||||
|
f"Внимание: колонка 'name' отсутствует в таблице для '{matched_key}'. Пропускаем добавление 'id'.")
|
||||||
|
continue # или обработать по-другому
|
||||||
|
else:
|
||||||
|
# Применяем функцию get_id_by_name к каждой строке в колонке 'name'
|
||||||
|
df_cleaned['id'] = df_cleaned['name'].apply(get_og_by_name)
|
||||||
|
|
||||||
|
# Удаляем строки, где id — None, NaN или пустой
|
||||||
|
df_cleaned = df_cleaned.dropna(subset=['id']) # dropna удаляет NaN
|
||||||
|
# Дополнительно: удаляем None (если не поймал dropna)
|
||||||
|
df_cleaned = df_cleaned[df_cleaned['id'].notna() & (df_cleaned['id'].astype(str) != 'None')]
|
||||||
|
|
||||||
|
# Добавляем в словарь
|
||||||
|
result_list.append(df_cleaned)
|
||||||
|
|
||||||
|
# === Объединение и сортировка по id (индекс) и table ===
|
||||||
|
if result_list:
|
||||||
|
combined_df = pd.concat(result_list, axis=0)
|
||||||
|
|
||||||
|
# Сортируем по индексу (id) и по столбцу 'table'
|
||||||
|
combined_df = combined_df.sort_values(by=['id', 'table'], axis=0)
|
||||||
|
|
||||||
|
# Устанавливаем id как индекс
|
||||||
|
# combined_df.set_index('id', inplace=True)
|
||||||
|
|
||||||
|
return combined_df
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def data_dict_to_json(self, data_dict):
|
def data_dict_to_json(self, data_dict):
|
||||||
''' Служебная функция для парсинга словаря в json. '''
|
''' Служебная функция для парсинга словаря в json. '''
|
||||||
@@ -308,17 +381,3 @@ class SvodkaCAParser(ParserPort):
|
|||||||
filtered_df = df[df['table'].isin(table_values)].copy()
|
filtered_df = df[df['table'].isin(table_values)].copy()
|
||||||
result_dict = {key: group for key, group in filtered_df.groupby('table')}
|
result_dict = {key: group for key, group in filtered_df.groupby('table')}
|
||||||
return result_dict
|
return result_dict
|
||||||
|
|
||||||
def get_value(self, df: pd.DataFrame, params: dict):
|
|
||||||
|
|
||||||
modes = params.get("modes")
|
|
||||||
tables = params.get("tables")
|
|
||||||
if not isinstance(modes, list):
|
|
||||||
raise ValueError("Поле 'modes' должно быть списком")
|
|
||||||
if not isinstance(tables, list):
|
|
||||||
raise ValueError("Поле 'tables' должно быть списком")
|
|
||||||
# Собираем данные
|
|
||||||
data_dict = {}
|
|
||||||
for mode in modes:
|
|
||||||
data_dict[mode] = self.get_data(df, mode, tables)
|
|
||||||
return self.data_dict_to_json(data_dict)
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
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_pm import SvodkaPMSingleOGRequest, SvodkaPMTotalOGsRequest
|
||||||
from adapters.pconfig import OG_IDS, replace_id_in_path, data_to_json
|
from adapters.pconfig import OG_IDS, replace_id_in_path, data_to_json
|
||||||
|
|
||||||
|
|
||||||
@@ -9,6 +11,57 @@ class SvodkaPMParser(ParserPort):
|
|||||||
|
|
||||||
name = "Сводки ПМ"
|
name = "Сводки ПМ"
|
||||||
|
|
||||||
|
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="Получение данных по одному ОГ"
|
||||||
|
)
|
||||||
|
|
||||||
|
register_getter_from_schema(
|
||||||
|
parser_instance=self,
|
||||||
|
getter_name="total_ogs",
|
||||||
|
method=self._get_total_ogs,
|
||||||
|
schema_class=SvodkaPMTotalOGsRequest,
|
||||||
|
description="Получение данных по всем ОГ"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Здесь нужно получить DataFrame из self.df, но пока используем старую логику
|
||||||
|
# TODO: Переделать под новую архитектуру
|
||||||
|
return self.get_svodka_og(self.df, og_id, codes, columns, search)
|
||||||
|
|
||||||
|
def _get_total_ogs(self, params: dict):
|
||||||
|
"""Получение данных по всем ОГ"""
|
||||||
|
# Валидируем параметры с помощью схемы Pydantic
|
||||||
|
validated_params = validate_params_with_schema(params, SvodkaPMTotalOGsRequest)
|
||||||
|
|
||||||
|
codes = validated_params["codes"]
|
||||||
|
columns = validated_params["columns"]
|
||||||
|
search = validated_params.get("search")
|
||||||
|
|
||||||
|
# TODO: Переделать под новую архитектуру
|
||||||
|
return self.get_svodka_total(self.df, codes, columns, search)
|
||||||
|
|
||||||
|
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||||
|
"""Парсинг файла и возврат DataFrame"""
|
||||||
|
# Сохраняем DataFrame для использования в геттерах
|
||||||
|
self.df = self.parse_svodka_pm_files(file_path, params)
|
||||||
|
return self.df
|
||||||
|
|
||||||
def find_header_row(self, file: str, sheet: str, search_value: str = "Итого", max_rows: int = 50) -> int:
|
def find_header_row(self, file: str, sheet: str, search_value: str = "Итого", max_rows: int = 50) -> int:
|
||||||
"""Определения индекса заголовка в excel по ключевому слову"""
|
"""Определения индекса заголовка в excel по ключевому слову"""
|
||||||
# Читаем первые max_rows строк без заголовков
|
# Читаем первые max_rows строк без заголовков
|
||||||
@@ -16,7 +69,8 @@ class SvodkaPMParser(ParserPort):
|
|||||||
file,
|
file,
|
||||||
sheet_name=sheet,
|
sheet_name=sheet,
|
||||||
header=None,
|
header=None,
|
||||||
nrows=max_rows
|
nrows=max_rows,
|
||||||
|
engine='openpyxl'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
||||||
@@ -40,6 +94,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:
|
||||||
@@ -61,7 +116,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:
|
||||||
@@ -99,25 +155,25 @@ class SvodkaPMParser(ParserPort):
|
|||||||
# Проверяем, является ли колонка пустой/некорректной
|
# Проверяем, является ли колонка пустой/некорректной
|
||||||
is_empty_or_unnamed = col_str.startswith('Unnamed') or col_str == '' or col_str.lower() == 'nan'
|
is_empty_or_unnamed = col_str.startswith('Unnamed') or col_str == '' or col_str.lower() == 'nan'
|
||||||
|
|
||||||
# Проверяем, начинается ли на "Итого"
|
if is_empty_or_unnamed:
|
||||||
if col_str.startswith('Итого'):
|
# Если это пустая колонка, используем последнее хорошее имя
|
||||||
current_name = 'Итого'
|
if last_good_name:
|
||||||
last_good_name = current_name # обновляем last_good_name
|
new_columns.append(last_good_name)
|
||||||
new_columns.append(current_name)
|
else:
|
||||||
elif is_empty_or_unnamed:
|
# Если нет хорошего имени, пропускаем
|
||||||
# Используем последнее хорошее имя
|
continue
|
||||||
new_columns.append(last_good_name)
|
|
||||||
else:
|
else:
|
||||||
# Имя, полученное из exel
|
# Это хорошая колонка
|
||||||
last_good_name = col_str
|
last_good_name = col_str
|
||||||
new_columns.append(col_str)
|
new_columns.append(col_str)
|
||||||
|
|
||||||
|
# Применяем новые заголовки
|
||||||
df_final.columns = new_columns
|
df_final.columns = new_columns
|
||||||
|
|
||||||
print(f"Окончательное количество столбцов: {len(df_final.columns)}")
|
|
||||||
return df_final
|
return df_final
|
||||||
|
|
||||||
def parse(self, file_path: str, params: dict) -> dict:
|
def parse_svodka_pm_files(self, zip_path: str, params: dict) -> dict:
|
||||||
|
"""Парсинг ZIP архива со сводками ПМ"""
|
||||||
import zipfile
|
import zipfile
|
||||||
pm_dict = {
|
pm_dict = {
|
||||||
"facts": {},
|
"facts": {},
|
||||||
@@ -125,7 +181,7 @@ class SvodkaPMParser(ParserPort):
|
|||||||
}
|
}
|
||||||
excel_fact_template = 'svodka_fact_pm_ID.xlsm'
|
excel_fact_template = 'svodka_fact_pm_ID.xlsm'
|
||||||
excel_plan_template = 'svodka_plan_pm_ID.xlsx'
|
excel_plan_template = 'svodka_plan_pm_ID.xlsx'
|
||||||
with zipfile.ZipFile(file_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 name, id in OG_IDS.items():
|
||||||
if id == 'BASH':
|
if id == 'BASH':
|
||||||
@@ -155,9 +211,9 @@ class SvodkaPMParser(ParserPort):
|
|||||||
|
|
||||||
return pm_dict
|
return pm_dict
|
||||||
|
|
||||||
def get_svodka_value(self, df_svodka, id, code, search_value=None):
|
def get_svodka_value(self, df_svodka, code, search_value, search_value_filter=None):
|
||||||
''' Служебная функция для простой выборке по сводке '''
|
''' Служебная функция получения значения по коду и столбцу '''
|
||||||
row_index = id
|
row_index = code
|
||||||
|
|
||||||
mask_value = df_svodka.iloc[0] == code
|
mask_value = df_svodka.iloc[0] == code
|
||||||
if search_value is None:
|
if search_value is None:
|
||||||
@@ -254,22 +310,4 @@ class SvodkaPMParser(ParserPort):
|
|||||||
|
|
||||||
return total_result
|
return total_result
|
||||||
|
|
||||||
def get_value(self, df, params):
|
# Убираем старый метод get_value, так как он теперь в базовом классе
|
||||||
og_id = params.get("id")
|
|
||||||
codes = params.get("codes")
|
|
||||||
columns = params.get("columns")
|
|
||||||
search = params.get("search")
|
|
||||||
mode = params.get("mode", "total")
|
|
||||||
if not isinstance(codes, list):
|
|
||||||
raise ValueError("Поле 'codes' должно быть списком")
|
|
||||||
if not isinstance(columns, list):
|
|
||||||
raise ValueError("Поле 'columns' должно быть списком")
|
|
||||||
data = None
|
|
||||||
if mode == "single":
|
|
||||||
if not og_id:
|
|
||||||
raise ValueError("Отсутствует идентификатор ОГ")
|
|
||||||
data = self.get_svodka_og(df, og_id, codes, columns, search)
|
|
||||||
elif mode == "total":
|
|
||||||
data = self.get_svodka_total(df, codes, columns, search)
|
|
||||||
json_result = data_to_json(data)
|
|
||||||
return json_result
|
|
||||||
|
|||||||
@@ -96,6 +96,54 @@ async def get_available_parsers():
|
|||||||
return {"parsers": parsers}
|
return {"parsers": parsers}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/parsers/{parser_name}/getters", tags=["Общее"],
|
||||||
|
summary="Информация о геттерах парсера",
|
||||||
|
description="Возвращает информацию о доступных геттерах для указанного парсера",
|
||||||
|
responses={
|
||||||
|
200: {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"example": {
|
||||||
|
"parser": "svodka_pm",
|
||||||
|
"getters": {
|
||||||
|
"single_og": {
|
||||||
|
"required_params": ["id", "codes", "columns"],
|
||||||
|
"optional_params": ["search"],
|
||||||
|
"description": "Получение данных по одному ОГ"
|
||||||
|
},
|
||||||
|
"total_ogs": {
|
||||||
|
"required_params": ["codes", "columns"],
|
||||||
|
"optional_params": ["search"],
|
||||||
|
"description": "Получение данных по всем ОГ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
"description": "Парсер не найден"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
async def get_parser_getters(parser_name: str):
|
||||||
|
"""Получение информации о геттерах парсера"""
|
||||||
|
if parser_name not in PARSERS:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"Парсер '{parser_name}' не найден"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser_class = PARSERS[parser_name]
|
||||||
|
parser_instance = parser_class()
|
||||||
|
|
||||||
|
getters_info = parser_instance.get_available_getters()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"parser": parser_name,
|
||||||
|
"getters": getters_info
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/server-info", tags=["Общее"],
|
@app.get("/server-info", tags=["Общее"],
|
||||||
summary="Информация о сервере",
|
summary="Информация о сервере",
|
||||||
response_model=ServerInfoResponse,)
|
response_model=ServerInfoResponse,)
|
||||||
@@ -352,40 +400,40 @@ 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])
|
@app.post("/svodka_pm/get_data", tags=[SvodkaPMParser.name])
|
||||||
# async def get_svodka_pm_data(
|
async def get_svodka_pm_data(
|
||||||
# request_data: dict
|
request_data: dict
|
||||||
# ):
|
):
|
||||||
# report_service = get_report_service()
|
report_service = get_report_service()
|
||||||
# """
|
"""
|
||||||
# Получение данных из отчета сводки факта СарНПЗ
|
Получение данных из отчета сводки факта СарНПЗ
|
||||||
|
|
||||||
# - indicator_id: ID индикатора
|
- indicator_id: ID индикатора
|
||||||
# - code: Код для поиска
|
- code: Код для поиска
|
||||||
# - search_value: Опциональное значение для поиска
|
- search_value: Опциональное значение для поиска
|
||||||
# """
|
"""
|
||||||
# try:
|
try:
|
||||||
# # Создаем запрос
|
# Создаем запрос
|
||||||
# request = DataRequest(
|
request = DataRequest(
|
||||||
# report_type='svodka_pm',
|
report_type='svodka_pm',
|
||||||
# get_params=request_data
|
get_params=request_data
|
||||||
# )
|
)
|
||||||
|
|
||||||
# # Получаем данные
|
# Получаем данные
|
||||||
# result = report_service.get_data(request)
|
result = report_service.get_data(request)
|
||||||
|
|
||||||
# if result.success:
|
if result.success:
|
||||||
# return {
|
return {
|
||||||
# "success": True,
|
"success": True,
|
||||||
# "data": result.data
|
"data": result.data
|
||||||
# }
|
}
|
||||||
# else:
|
else:
|
||||||
# raise HTTPException(status_code=404, detail=result.message)
|
raise HTTPException(status_code=404, detail=result.message)
|
||||||
|
|
||||||
# except HTTPException:
|
except HTTPException:
|
||||||
# raise
|
raise
|
||||||
# except Exception as e:
|
except Exception as e:
|
||||||
# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(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],
|
||||||
@@ -562,38 +610,38 @@ 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])
|
@app.post("/monitoring_fuel/get_data", tags=[MonitoringFuelParser.name])
|
||||||
# async def get_monitoring_fuel_data(
|
async def get_monitoring_fuel_data(
|
||||||
# request_data: dict
|
request_data: dict
|
||||||
# ):
|
):
|
||||||
# report_service = get_report_service()
|
report_service = get_report_service()
|
||||||
# """
|
"""
|
||||||
# Получение данных из отчета мониторинга топлива
|
Получение данных из отчета мониторинга топлива
|
||||||
|
|
||||||
# - column: Название колонки для агрегации (normativ, total, total_svod)
|
- column: Название колонки для агрегации (normativ, total, total_svod)
|
||||||
# """
|
"""
|
||||||
# try:
|
try:
|
||||||
# # Создаем запрос
|
# Создаем запрос
|
||||||
# request = DataRequest(
|
request = DataRequest(
|
||||||
# report_type='monitoring_fuel',
|
report_type='monitoring_fuel',
|
||||||
# get_params=request_data
|
get_params=request_data
|
||||||
# )
|
)
|
||||||
|
|
||||||
# # Получаем данные
|
# Получаем данные
|
||||||
# result = report_service.get_data(request)
|
result = report_service.get_data(request)
|
||||||
|
|
||||||
# if result.success:
|
if result.success:
|
||||||
# return {
|
return {
|
||||||
# "success": True,
|
"success": True,
|
||||||
# "data": result.data
|
"data": result.data
|
||||||
# }
|
}
|
||||||
# else:
|
else:
|
||||||
# raise HTTPException(status_code=404, detail=result.message)
|
raise HTTPException(status_code=404, detail=result.message)
|
||||||
|
|
||||||
# except HTTPException:
|
except HTTPException:
|
||||||
# raise
|
raise
|
||||||
# except Exception as e:
|
except Exception as e:
|
||||||
# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(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])
|
||||||
|
|||||||
Binary file not shown.
@@ -2,28 +2,93 @@
|
|||||||
Порты (интерфейсы) для hexagonal architecture
|
Порты (интерфейсы) для hexagonal architecture
|
||||||
"""
|
"""
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Optional
|
from typing import Optional, Dict, List, Any, Callable
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
class ParserPort(ABC):
|
class ParserPort(ABC):
|
||||||
"""Интерфейс для парсеров"""
|
"""Интерфейс для парсеров с поддержкой множественных геттеров"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Инициализация с пустым словарем геттеров"""
|
||||||
|
self.getters: Dict[str, Dict[str, Any]] = {}
|
||||||
|
self._register_default_getters()
|
||||||
|
|
||||||
|
def _register_default_getters(self):
|
||||||
|
"""Регистрация геттеров по умолчанию - переопределяется в наследниках"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def register_getter(self, name: str, method: Callable, required_params: List[str],
|
||||||
|
optional_params: List[str] = None, description: str = ""):
|
||||||
|
"""
|
||||||
|
Регистрация нового геттера
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Имя геттера
|
||||||
|
method: Метод для выполнения
|
||||||
|
required_params: Список обязательных параметров
|
||||||
|
optional_params: Список необязательных параметров
|
||||||
|
description: Описание геттера
|
||||||
|
"""
|
||||||
|
if optional_params is None:
|
||||||
|
optional_params = []
|
||||||
|
|
||||||
|
self.getters[name] = {
|
||||||
|
"method": method,
|
||||||
|
"required_params": required_params,
|
||||||
|
"optional_params": optional_params,
|
||||||
|
"description": description
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_available_getters(self) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""Получение списка доступных геттеров с их описанием"""
|
||||||
|
return {
|
||||||
|
name: {
|
||||||
|
"required_params": info["required_params"],
|
||||||
|
"optional_params": info["optional_params"],
|
||||||
|
"description": info["description"]
|
||||||
|
}
|
||||||
|
for name, info in self.getters.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавить схему
|
||||||
|
def get_value(self, getter_name: str, params: Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Получение значения через указанный геттер
|
||||||
|
|
||||||
|
Args:
|
||||||
|
getter_name: Имя геттера
|
||||||
|
params: Параметры для геттера
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Результат выполнения геттера
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Если геттер не найден или параметры неверны
|
||||||
|
"""
|
||||||
|
if getter_name not in self.getters:
|
||||||
|
available = list(self.getters.keys())
|
||||||
|
raise ValueError(f"Геттер '{getter_name}' не найден. Доступные: {available}")
|
||||||
|
|
||||||
|
getter_info = self.getters[getter_name]
|
||||||
|
required = getter_info["required_params"]
|
||||||
|
|
||||||
|
# Проверка обязательных параметров
|
||||||
|
missing = [p for p in required if p not in params]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"Отсутствуют обязательные параметры для геттера '{getter_name}': {missing}")
|
||||||
|
|
||||||
|
# Вызов метода геттера
|
||||||
|
try:
|
||||||
|
return getter_info["method"](params)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Ошибка выполнения геттера '{getter_name}': {str(e)}")
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||||
"""Парсинг файла и возврат DataFrame"""
|
"""Парсинг файла и возврат DataFrame"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_value(self, df: pd.DataFrame, params: dict):
|
|
||||||
"""Получение значения из DataFrame по параметрам"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
# @abstractmethod
|
|
||||||
# def get_schema(self) -> dict:
|
|
||||||
# """Возвращает схему входных параметров для парсера"""
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
class StoragePort(ABC):
|
class StoragePort(ABC):
|
||||||
"""Интерфейс для хранилища данных"""
|
"""Интерфейс для хранилища данных"""
|
||||||
|
|||||||
140
python_parser/core/schema_utils.py
Normal file
140
python_parser/core/schema_utils.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"""
|
||||||
|
Упрощенные утилиты для работы со схемами 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)
|
||||||
|
return validated_data.dict()
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Ошибка валидации параметров: {str(e)}")
|
||||||
@@ -99,9 +99,35 @@ class ReportService:
|
|||||||
|
|
||||||
# Получаем парсер
|
# Получаем парсер
|
||||||
parser = get_parser(request.report_type)
|
parser = get_parser(request.report_type)
|
||||||
|
|
||||||
|
# Устанавливаем DataFrame в парсер для использования в геттерах
|
||||||
|
parser.df = df
|
||||||
|
|
||||||
# Получаем значение
|
# Получаем параметры запроса
|
||||||
value = parser.get_value(df, request.get_params)
|
get_params = request.get_params or {}
|
||||||
|
|
||||||
|
# Определяем имя геттера (по умолчанию используем первый доступный)
|
||||||
|
getter_name = get_params.pop("getter", None)
|
||||||
|
if not getter_name:
|
||||||
|
# Если геттер не указан, берем первый доступный
|
||||||
|
available_getters = list(parser.getters.keys())
|
||||||
|
if available_getters:
|
||||||
|
getter_name = available_getters[0]
|
||||||
|
print(f"⚠️ Геттер не указан, используем первый доступный: {getter_name}")
|
||||||
|
else:
|
||||||
|
return DataResult(
|
||||||
|
success=False,
|
||||||
|
message="Парсер не имеет доступных геттеров"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Получаем значение через указанный геттер
|
||||||
|
try:
|
||||||
|
value = parser.get_value(getter_name, get_params)
|
||||||
|
except ValueError as e:
|
||||||
|
return DataResult(
|
||||||
|
success=False,
|
||||||
|
message=f"Ошибка параметров: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
# Формируем результат
|
# Формируем результат
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
|||||||
@@ -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,51 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Запуск Streamlit интерфейса для NIN Excel Parsers API
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import webbrowser
|
|
||||||
import time
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Основная функция"""
|
|
||||||
print("🚀 ЗАПУСК STREAMLIT ИНТЕРФЕЙСА")
|
|
||||||
print("=" * 50)
|
|
||||||
print("Убедитесь, что FastAPI сервер запущен на порту 8000")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Проверяем, установлен ли Streamlit
|
|
||||||
try:
|
|
||||||
import streamlit
|
|
||||||
print(f"✅ Streamlit {streamlit.__version__} установлен")
|
|
||||||
except ImportError:
|
|
||||||
print("❌ Streamlit не установлен")
|
|
||||||
print("Установите: pip install streamlit")
|
|
||||||
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", "streamlit_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.*
|
|
||||||
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:
|
||||||
@@ -254,8 +255,8 @@ def main():
|
|||||||
|
|
||||||
modes = st.multiselect(
|
modes = st.multiselect(
|
||||||
"Выберите режимы",
|
"Выберите режимы",
|
||||||
["План", "Факт", "Норматив"],
|
["plan", "fact", "normativ"],
|
||||||
default=["План", "Факт"],
|
default=["plan", "fact"],
|
||||||
key="ca_modes"
|
key="ca_modes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -373,7 +374,7 @@ def main():
|
|||||||
# Футер
|
# Футер
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("### 📚 Документация API")
|
st.markdown("### 📚 Документация API")
|
||||||
st.markdown(f"Полная документация доступна по адресу: {API_BASE_URL}/docs")
|
st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs")
|
||||||
|
|
||||||
# Информация о проекте
|
# Информация о проекте
|
||||||
with st.expander("ℹ️ О проекте"):
|
with st.expander("ℹ️ О проекте"):
|
||||||
Reference in New Issue
Block a user