Compare commits
3 Commits
34937ec062
...
fix-1
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cbdaf1b60 | |||
| 9459196804 | |||
| ce228d9756 |
153
.gitignore
vendored
Normal file
153
.gitignore
vendored
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
data
|
||||||
|
.streamlit
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
data/
|
||||||
|
*.zip
|
||||||
|
*.xlsx
|
||||||
|
*.xls
|
||||||
|
*.xlsm
|
||||||
|
|
||||||
|
# MinIO data directory
|
||||||
|
minio_data/
|
||||||
182
README.md
Normal file
182
README.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# 🚀 NIN Excel Parsers API - Полная система
|
||||||
|
|
||||||
|
Полноценная система для парсинга Excel отчетов нефтеперерабатывающих заводов (НПЗ) с использованием FastAPI, MinIO и Streamlit.
|
||||||
|
|
||||||
|
## 🏗️ Архитектура проекта
|
||||||
|
|
||||||
|
Проект состоит из **двух изолированных пакетов**:
|
||||||
|
|
||||||
|
- **`python_parser/`** - FastAPI сервер + парсеры Excel
|
||||||
|
- **`streamlit_app/`** - Веб-интерфейс для демонстрации API
|
||||||
|
|
||||||
|
## 🚀 Быстрый запуск
|
||||||
|
|
||||||
|
### **Вариант 1: Все сервисы в Docker (рекомендуется)**
|
||||||
|
```bash
|
||||||
|
# Запуск всех сервисов: MinIO + FastAPI + Streamlit
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Доступ:
|
||||||
|
# - MinIO Console: http://localhost:9001
|
||||||
|
# - FastAPI: http://localhost:8000
|
||||||
|
# - Streamlit: http://localhost:8501
|
||||||
|
# - API Docs: http://localhost:8000/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Вариант 2: Только MinIO в Docker + сервисы локально**
|
||||||
|
```bash
|
||||||
|
# Запуск MinIO в Docker
|
||||||
|
docker-compose up -d minio
|
||||||
|
|
||||||
|
# Запуск FastAPI локально
|
||||||
|
cd python_parser
|
||||||
|
python run_dev.py
|
||||||
|
|
||||||
|
# В отдельном терминале - Streamlit
|
||||||
|
cd streamlit_app
|
||||||
|
streamlit run app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Вариант 3: Только MinIO в Docker**
|
||||||
|
```bash
|
||||||
|
# Запуск только MinIO
|
||||||
|
docker-compose up -d minio
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Описание сервисов
|
||||||
|
|
||||||
|
- **MinIO** (порт 9000-9001): S3-совместимое хранилище для данных
|
||||||
|
- **FastAPI** (порт 8000): API сервер для парсинга Excel файлов
|
||||||
|
- **Streamlit** (порт 8501): Веб-интерфейс для демонстрации API
|
||||||
|
|
||||||
|
## 📁 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
python_parser_cf/ # Корень проекта
|
||||||
|
├── python_parser/ # Пакет FastAPI + парсеры
|
||||||
|
│ ├── app/ # FastAPI приложение
|
||||||
|
│ │ ├── main.py # Основной файл приложения
|
||||||
|
│ │ └── schemas/ # Pydantic схемы
|
||||||
|
│ ├── core/ # Бизнес-логика
|
||||||
|
│ │ ├── models.py # Модели данных
|
||||||
|
│ │ ├── ports.py # Интерфейсы (порты)
|
||||||
|
│ │ └── services.py # Сервисы
|
||||||
|
│ ├── adapters/ # Адаптеры для внешних систем
|
||||||
|
│ │ ├── storage.py # MinIO адаптер
|
||||||
|
│ │ └── parsers/ # Парсеры Excel файлов
|
||||||
|
│ ├── data/ # Тестовые данные
|
||||||
|
│ ├── Dockerfile # Docker образ для FastAPI
|
||||||
|
│ ├── requirements.txt # Зависимости FastAPI
|
||||||
|
│ └── run_dev.py # Запуск FastAPI локально
|
||||||
|
├── streamlit_app/ # Пакет Streamlit
|
||||||
|
│ ├── app.py # Основное Streamlit приложение
|
||||||
|
│ ├── requirements.txt # Зависимости Streamlit
|
||||||
|
│ ├── Dockerfile # Docker образ для Streamlit
|
||||||
|
│ ├── .streamlit/ # Конфигурация Streamlit
|
||||||
|
│ │ └── config.toml # Настройки
|
||||||
|
│ └── README.md # Документация Streamlit
|
||||||
|
├── docker-compose.yml # Docker Compose конфигурация
|
||||||
|
├── .gitignore # Git исключения
|
||||||
|
└── README.md # Общая документация
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Доступные эндпоинты
|
||||||
|
|
||||||
|
- **GET /** - Информация об API
|
||||||
|
- **GET /docs** - Swagger документация
|
||||||
|
- **GET /parsers** - Список доступных парсеров
|
||||||
|
- **GET /parsers/{parser_name}/getters** - Информация о геттерах парсера
|
||||||
|
- **POST /svodka_pm/upload-zip** - Загрузка сводок ПМ
|
||||||
|
- **POST /svodka_ca/upload** - Загрузка сводок ЦА
|
||||||
|
- **POST /monitoring_fuel/upload-zip** - Загрузка мониторинга топлива
|
||||||
|
- **POST /svodka_pm/get_data** - Получение данных сводок ПМ
|
||||||
|
- **POST /svodka_ca/get_data** - Получение данных сводок ЦА
|
||||||
|
- **POST /monitoring_fuel/get_data** - Получение данных мониторинга топлива
|
||||||
|
|
||||||
|
## 📊 Поддерживаемые типы отчетов
|
||||||
|
|
||||||
|
1. **svodka_pm** - Сводки по переработке нефти (ПМ)
|
||||||
|
- Геттеры: `single_og`, `total_ogs`
|
||||||
|
2. **svodka_ca** - Сводки по переработке нефти (ЦА)
|
||||||
|
- Геттеры: `get_data`
|
||||||
|
3. **monitoring_fuel** - Мониторинг топлива
|
||||||
|
- Геттеры: `total_by_columns`, `month_by_code`
|
||||||
|
|
||||||
|
## 🏗️ Архитектура
|
||||||
|
|
||||||
|
Проект использует **Hexagonal Architecture (Ports and Adapters)**:
|
||||||
|
|
||||||
|
- **Порты (Ports)**: Интерфейсы для бизнес-логики
|
||||||
|
- **Адаптеры (Adapters)**: Реализации для внешних систем
|
||||||
|
- **Сервисы (Services)**: Бизнес-логика приложения
|
||||||
|
|
||||||
|
### Система геттеров парсеров
|
||||||
|
|
||||||
|
Каждый парсер может иметь несколько методов получения данных (геттеров):
|
||||||
|
- Регистрация геттеров в словаре с метаданными
|
||||||
|
- Валидация параметров для каждого геттера
|
||||||
|
- Единый интерфейс `get_value(getter_name, params)`
|
||||||
|
|
||||||
|
## 🐳 Docker
|
||||||
|
|
||||||
|
### Сборка образов:
|
||||||
|
```bash
|
||||||
|
# FastAPI
|
||||||
|
docker build -t nin-fastapi ./python_parser
|
||||||
|
|
||||||
|
# Streamlit
|
||||||
|
docker build -t nin-streamlit ./streamlit_app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск отдельных сервисов:
|
||||||
|
```bash
|
||||||
|
# Только MinIO
|
||||||
|
docker-compose up -d minio
|
||||||
|
|
||||||
|
# MinIO + FastAPI
|
||||||
|
docker-compose up -d minio fastapi
|
||||||
|
|
||||||
|
# Все сервисы
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛑 Остановка
|
||||||
|
|
||||||
|
### Остановка Docker сервисов:
|
||||||
|
```bash
|
||||||
|
# Все сервисы
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Только MinIO
|
||||||
|
docker-compose stop minio
|
||||||
|
```
|
||||||
|
|
||||||
|
### Остановка локальных сервисов:
|
||||||
|
```bash
|
||||||
|
# Нажмите Ctrl+C в терминале с FastAPI/Streamlit
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Разработка
|
||||||
|
|
||||||
|
### Добавление нового парсера:
|
||||||
|
|
||||||
|
1. Создайте файл в `python_parser/adapters/parsers/`
|
||||||
|
2. Реализуйте интерфейс `ParserPort`
|
||||||
|
3. Добавьте в `python_parser/core/services.py`
|
||||||
|
4. Создайте схемы в `python_parser/app/schemas/`
|
||||||
|
5. Добавьте эндпоинты в `python_parser/app/main.py`
|
||||||
|
|
||||||
|
### Тестирование:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запуск тестов
|
||||||
|
cd python_parser
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Запуск с покрытием
|
||||||
|
pytest --cov=.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Лицензия
|
||||||
|
|
||||||
|
Проект разработан для внутреннего использования НИН.
|
||||||
@@ -170,16 +170,11 @@ def main():
|
|||||||
|
|
||||||
if not port_8000_ok:
|
if not port_8000_ok:
|
||||||
print("\n🔧 РЕШЕНИЕ: Запустите FastAPI сервер")
|
print("\n🔧 РЕШЕНИЕ: Запустите FastAPI сервер")
|
||||||
print("python run_dev.py")
|
print("docker-compose up -d fastapi")
|
||||||
|
|
||||||
if not port_8501_ok:
|
if not port_8501_ok:
|
||||||
print("\n🔧 РЕШЕНИЕ: Запустите Streamlit")
|
print("\n🔧 РЕШЕНИЕ: Запустите Streamlit")
|
||||||
print("python run_streamlit.py")
|
print("docker-compose up -d streamlit")
|
||||||
|
|
||||||
print("\n🚀 Для автоматического запуска используйте:")
|
|
||||||
print("python start_demo.py")
|
|
||||||
print("\n🔍 Для пошагового запуска используйте:")
|
|
||||||
print("python run_manual.py")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
34
create_test_excel.py
Normal file
34
create_test_excel.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Создание тестового Excel файла для тестирования API
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def create_test_excel():
|
||||||
|
"""Создание тестового Excel файла"""
|
||||||
|
|
||||||
|
# Создаем тестовые данные
|
||||||
|
data = {
|
||||||
|
'name': ['Установка 1', 'Установка 2', 'Установка 3'],
|
||||||
|
'normativ': [100, 200, 300],
|
||||||
|
'total': [95, 195, 295],
|
||||||
|
'total_1': [90, 190, 290]
|
||||||
|
}
|
||||||
|
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
|
||||||
|
# Сохраняем в Excel
|
||||||
|
filename = 'test_file.xlsx'
|
||||||
|
with pd.ExcelWriter(filename, engine='openpyxl') as writer:
|
||||||
|
df.to_excel(writer, sheet_name='Мониторинг потребления', index=False)
|
||||||
|
|
||||||
|
print(f"✅ Тестовый файл создан: {filename}")
|
||||||
|
print(f"📊 Содержимое: {len(df)} строк, {len(df.columns)} столбцов")
|
||||||
|
print(f"📋 Столбцы: {list(df.columns)}")
|
||||||
|
|
||||||
|
return filename
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_test_excel()
|
||||||
@@ -10,11 +10,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 +28,14 @@ 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
|
||||||
|
- DOCKER_ENV=true
|
||||||
|
depends_on:
|
||||||
|
- 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
|
|
||||||
20
python_parser/Dockerfile_
Normal file
20
python_parser/Dockerfile_
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM repo-dev.predix.rosneft.ru/python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# RUN pip install kafka-python==2.0.2
|
||||||
|
# RUN pip freeze > /app/requirements.txt
|
||||||
|
|
||||||
|
# ADD . /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN mkdir -p vendor
|
||||||
|
RUN pip download -r /app/requirements.txt --no-binary=:none: -d /app/vendor
|
||||||
|
|
||||||
|
# ADD . /app
|
||||||
|
|
||||||
|
# ENV KAFKA_BROKER=10.234.160.10:9093,10.234.160.10:9094,10.234.160.10:9095
|
||||||
|
# ENV KAFKA_UPDATE_ALGORITHM_RULES_TOPIC=algorithm-rule-update
|
||||||
|
# ENV KAFKA_CLIENT_USERNAME=cf-service
|
||||||
|
|
||||||
|
# CMD ["python", "/app/run_dev.py"]
|
||||||
@@ -1,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,66 +1,28 @@
|
|||||||
# NIN Excel Parsers API
|
# 📊 Python Parser - FastAPI + Парсеры Excel
|
||||||
|
|
||||||
API для парсинга Excel отчетов нефтеперерабатывающих заводов (НПЗ) с использованием FastAPI и MinIO для хранения данных.
|
Пакет FastAPI сервера и парсеров Excel для нефтеперерабатывающих заводов.
|
||||||
|
|
||||||
## 🚀 Быстрый запуск
|
## 🚀 Быстрый запуск
|
||||||
|
|
||||||
### **Вариант 1: Только MinIO в Docker + FastAPI локально**
|
### **Локально:**
|
||||||
```bash
|
```bash
|
||||||
# Запуск MinIO в Docker
|
# Установка зависимостей
|
||||||
docker-compose up -d minio
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# Запуск FastAPI локально
|
# Запуск FastAPI сервера
|
||||||
python run_dev.py
|
python run_dev.py
|
||||||
|
|
||||||
# В отдельном терминале запуск Streamlit
|
|
||||||
python run_streamlit.py
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Вариант 2: MinIO + FastAPI в Docker + Streamlit локально**
|
### **В Docker:**
|
||||||
```bash
|
```bash
|
||||||
# Запуск MinIO и FastAPI в Docker
|
# Сборка образа
|
||||||
docker-compose up -d
|
docker build -t nin-fastapi .
|
||||||
|
|
||||||
# В отдельном терминале запуск Streamlit
|
# Запуск контейнера
|
||||||
python run_streamlit.py
|
docker run -p 8000:8000 nin-fastapi
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Вариант 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/
|
python_parser/
|
||||||
@@ -75,104 +37,49 @@ python_parser/
|
|||||||
│ ├── storage.py # MinIO адаптер
|
│ ├── storage.py # MinIO адаптер
|
||||||
│ └── parsers/ # Парсеры Excel файлов
|
│ └── parsers/ # Парсеры Excel файлов
|
||||||
├── data/ # Тестовые данные
|
├── data/ # Тестовые данные
|
||||||
├── docker-compose.yml # Docker Compose конфигурация
|
|
||||||
├── Dockerfile # Docker образ для FastAPI
|
├── Dockerfile # Docker образ для FastAPI
|
||||||
├── run_dev.py # Запуск FastAPI локально
|
├── requirements.txt # Зависимости Python
|
||||||
├── run_streamlit.py # Запуск Streamlit
|
└── run_dev.py # Запуск FastAPI локально
|
||||||
└── check_services.py # Диагностика сервисов
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔍 Доступные эндпоинты
|
## 🔍 Основные эндпоинты
|
||||||
|
|
||||||
- **GET /** - Информация об API
|
- **GET /** - Информация об API
|
||||||
- **GET /docs** - Swagger документация
|
- **GET /docs** - Swagger документация
|
||||||
|
- **GET /parsers** - Список доступных парсеров
|
||||||
|
- **GET /parsers/{parser_name}/getters** - Информация о геттерах парсера
|
||||||
- **POST /svodka_pm/upload-zip** - Загрузка сводок ПМ
|
- **POST /svodka_pm/upload-zip** - Загрузка сводок ПМ
|
||||||
- **POST /svodka_ca/upload-zip** - Загрузка сводок ЦА
|
- **POST /svodka_ca/upload** - Загрузка сводок ЦА
|
||||||
- **POST /monitoring_fuel/upload-zip** - Загрузка мониторинга топлива
|
- **POST /monitoring_fuel/upload-zip** - Загрузка мониторинга топлива
|
||||||
- **GET /svodka_pm/data** - Получение данных сводок ПМ
|
- **POST /svodka_pm/get_data** - Получение данных сводок ПМ
|
||||||
- **GET /svodka_ca/data** - Получение данных сводок ЦА
|
- **POST /svodka_ca/get_data** - Получение данных сводок ЦА
|
||||||
- **GET /monitoring_fuel/data** - Получение данных мониторинга топлива
|
- **POST /monitoring_fuel/get_data** - Получение данных мониторинга топлива
|
||||||
|
|
||||||
## 📊 Поддерживаемые типы отчетов
|
## 📊 Поддерживаемые парсеры
|
||||||
|
|
||||||
1. **svodka_pm** - Сводки по переработке нефти (ПМ)
|
1. **svodka_pm** - Сводки по переработке нефти (ПМ)
|
||||||
|
- Геттеры: `single_og`, `total_ogs`
|
||||||
2. **svodka_ca** - Сводки по переработке нефти (ЦА)
|
2. **svodka_ca** - Сводки по переработке нефти (ЦА)
|
||||||
|
- Геттеры: `get_data`
|
||||||
3. **monitoring_fuel** - Мониторинг топлива
|
3. **monitoring_fuel** - Мониторинг топлива
|
||||||
|
- Геттеры: `total_by_columns`, `month_by_code`
|
||||||
|
|
||||||
## 🐳 Docker команды
|
## 🏗️ Архитектура
|
||||||
|
|
||||||
### Сборка и запуск:
|
Использует **Hexagonal Architecture (Ports and Adapters)**:
|
||||||
```bash
|
|
||||||
# Все сервисы
|
|
||||||
docker-compose up -d --build
|
|
||||||
|
|
||||||
# Только MinIO
|
- **Порты (Ports)**: Интерфейсы для бизнес-логики
|
||||||
docker-compose up -d minio
|
- **Адаптеры (Adapters)**: Реализации для внешних систем
|
||||||
|
- **Сервисы (Services)**: Бизнес-логика приложения
|
||||||
|
|
||||||
# Только FastAPI (требует MinIO)
|
### Система геттеров парсеров
|
||||||
docker-compose up -d fastapi
|
|
||||||
```
|
|
||||||
|
|
||||||
### Просмотр логов:
|
Каждый парсер может иметь несколько методов получения данных (геттеров):
|
||||||
```bash
|
- Регистрация геттеров в словаре с метаданными
|
||||||
# Все сервисы
|
- Валидация параметров для каждого геттера
|
||||||
docker-compose logs
|
- Единый интерфейс `get_value(getter_name, params)`
|
||||||
|
|
||||||
# Конкретный сервис
|
## 🔧 Разработка
|
||||||
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. Или используйте другой порт в конфигурации
|
|
||||||
|
|
||||||
## 🚀 Разработка
|
|
||||||
|
|
||||||
### Добавление нового парсера:
|
### Добавление нового парсера:
|
||||||
|
|
||||||
@@ -192,6 +99,6 @@ pytest
|
|||||||
pytest --cov=.
|
pytest --cov=.
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📝 Лицензия
|
## 📝 Примечание
|
||||||
|
|
||||||
Проект разработан для внутреннего использования НИН.
|
Этот пакет является частью большей системы. Для полной документации и запуска всех сервисов см. README.md в корне проекта.
|
||||||
@@ -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
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
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 adapters.pconfig import data_to_json
|
||||||
|
|
||||||
|
|
||||||
class MonitoringFuelParser(ParserPort):
|
class MonitoringFuelParser(ParserPort):
|
||||||
@@ -11,71 +11,55 @@ 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 строк без заголовков
|
self.register_getter(
|
||||||
df_temp = pd.read_excel(
|
name="total_by_columns",
|
||||||
file_path,
|
method=self._get_total_by_columns,
|
||||||
sheet_name=sheet,
|
required_params=["columns"],
|
||||||
header=None,
|
optional_params=[],
|
||||||
nrows=max_rows
|
description="Агрегация данных по колонкам"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
|
self.register_getter(
|
||||||
for idx, row in df_temp.iterrows():
|
name="month_by_code",
|
||||||
if row.astype(str).str.strip().str.contains(f"^{search_value}$", case=False, regex=True).any():
|
method=self._get_month_by_code,
|
||||||
print(f"Заголовок найден в строке {idx} (Excel: {idx + 1})")
|
required_params=["month"],
|
||||||
return idx + 1 # возвращаем индекс строки (0-based)
|
optional_params=[],
|
||||||
|
description="Получение данных за конкретный месяц"
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# === Удаление полностью пустых столбцов ===
|
def _get_total_by_columns(self, params: dict):
|
||||||
df_clean = df_full.replace(r'^\s*$', pd.NA, regex=True) # заменяем пустые строки на NA
|
"""Агрегация по колонкам (обертка для совместимости)"""
|
||||||
df_clean = df_clean.dropna(axis=1, how='all') # удаляем столбцы, где все значения — NA
|
columns = params["columns"]
|
||||||
df_full = df_full.loc[:, df_clean.columns] # оставляем только непустые столбцы
|
if not columns:
|
||||||
|
raise ValueError("Отсутствуют идентификаторы столбцов")
|
||||||
|
|
||||||
# === Переименовываем нужные столбцы по позициям ===
|
# TODO: Переделать под новую архитектуру
|
||||||
if len(df_full.columns) < 2:
|
df_means, _ = self.aggregate_by_columns(self.df, columns)
|
||||||
raise ValueError("DataFrame должен содержать как минимум 2 столбца.")
|
return df_means.to_dict(orient='index')
|
||||||
|
|
||||||
new_columns = df_full.columns.tolist()
|
def _get_month_by_code(self, params: dict):
|
||||||
|
"""Получение данных за месяц (обертка для совместимости)"""
|
||||||
|
month = params["month"]
|
||||||
|
if not month:
|
||||||
|
raise ValueError("Отсутствует идентификатор месяца")
|
||||||
|
|
||||||
new_columns[0] = 'name'
|
# TODO: Переделать под новую архитектуру
|
||||||
new_columns[1] = 'normativ'
|
df_month = self.get_month(self.df, month)
|
||||||
new_columns[-2] = 'total'
|
return df_month.to_dict(orient='index')
|
||||||
new_columns[-1] = 'total_1'
|
|
||||||
|
|
||||||
df_full.columns = new_columns
|
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||||
|
"""Парсинг файла и возврат DataFrame"""
|
||||||
|
# Сохраняем DataFrame для использования в геттерах
|
||||||
|
self.df = self.parse_monitoring_fuel_files(file_path, params)
|
||||||
|
return self.df
|
||||||
|
|
||||||
# Проверяем, что колонка 'name' существует
|
def parse_monitoring_fuel_files(self, zip_path: str, params: dict) -> Dict[str, pd.DataFrame]:
|
||||||
if 'name' in df_full.columns:
|
"""Парсинг ZIP архива с файлами мониторинга топлива"""
|
||||||
# Применяем функцию 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 +87,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 +232,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
|
|
||||||
|
|||||||
@@ -6,85 +6,48 @@ 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)
|
self.register_getter(
|
||||||
df_filled = df.fillna('')
|
name="get_data",
|
||||||
df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True)
|
method=self._get_data_wrapper,
|
||||||
|
required_params=["modes", "tables"],
|
||||||
|
optional_params=[],
|
||||||
|
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))
|
"""Обертка для получения данных (для совместимости)"""
|
||||||
|
modes = params["modes"]
|
||||||
|
tables = params["tables"]
|
||||||
|
|
||||||
row_indices = non_empty_rows[non_empty_rows].index.tolist()
|
if not isinstance(modes, list):
|
||||||
col_indices = non_empty_cols[non_empty_cols].index.tolist()
|
raise ValueError("Поле 'modes' должно быть списком")
|
||||||
|
if not isinstance(tables, list):
|
||||||
|
raise ValueError("Поле 'tables' должно быть списком")
|
||||||
|
|
||||||
if not row_indices or not col_indices:
|
# TODO: Переделать под новую архитектуру
|
||||||
return []
|
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_blocks = self._get_contiguous_blocks(row_indices)
|
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||||
col_blocks = self._get_contiguous_blocks(col_indices)
|
"""Парсинг файла и возврат DataFrame"""
|
||||||
|
# Сохраняем DataFrame для использования в геттерах
|
||||||
|
self.df = self.parse_svodka_ca(file_path, params)
|
||||||
|
return self.df
|
||||||
|
|
||||||
tables = []
|
def parse_svodka_ca(self, file_path: str, params: dict) -> dict:
|
||||||
for r_start, r_end in row_blocks:
|
"""Парсинг сводки СА"""
|
||||||
for c_start, c_end in col_blocks:
|
# Получаем параметры из params
|
||||||
block = df.iloc[r_start:r_end + 1, c_start:c_end + 1]
|
sheet_name = params.get('sheet_name', 0) # По умолчанию первый лист
|
||||||
if block.empty or block.fillna('').astype(str).replace(r'^\s*$', '', regex=True).eq('').all().all():
|
inclusion_list = params.get('inclusion_list', {'ТиП', 'Топливо', 'Потери'})
|
||||||
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 +153,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 +380,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)
|
|
||||||
|
|||||||
@@ -9,6 +9,60 @@ class SvodkaPMParser(ParserPort):
|
|||||||
|
|
||||||
name = "Сводки ПМ"
|
name = "Сводки ПМ"
|
||||||
|
|
||||||
|
def _register_default_getters(self):
|
||||||
|
"""Регистрация геттеров по умолчанию"""
|
||||||
|
self.register_getter(
|
||||||
|
name="single_og",
|
||||||
|
method=self._get_single_og,
|
||||||
|
required_params=["id", "codes", "columns"],
|
||||||
|
optional_params=["search"],
|
||||||
|
description="Получение данных по одному ОГ"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.register_getter(
|
||||||
|
name="total_ogs",
|
||||||
|
method=self._get_total_ogs,
|
||||||
|
required_params=["codes", "columns"],
|
||||||
|
optional_params=["search"],
|
||||||
|
description="Получение данных по всем ОГ"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_single_og(self, params: dict):
|
||||||
|
"""Получение данных по одному ОГ (обертка для совместимости)"""
|
||||||
|
og_id = params["id"]
|
||||||
|
codes = params["codes"]
|
||||||
|
columns = params["columns"]
|
||||||
|
search = params.get("search")
|
||||||
|
|
||||||
|
if not isinstance(codes, list):
|
||||||
|
raise ValueError("Поле 'codes' должно быть списком")
|
||||||
|
if not isinstance(columns, list):
|
||||||
|
raise ValueError("Поле 'columns' должно быть списком")
|
||||||
|
|
||||||
|
# Здесь нужно получить DataFrame из self.df, но пока используем старую логику
|
||||||
|
# TODO: Переделать под новую архитектуру
|
||||||
|
return self.get_svodka_og(self.df, og_id, codes, columns, search)
|
||||||
|
|
||||||
|
def _get_total_ogs(self, params: dict):
|
||||||
|
"""Получение данных по всем ОГ (обертка для совместимости)"""
|
||||||
|
codes = params["codes"]
|
||||||
|
columns = params["columns"]
|
||||||
|
search = params.get("search")
|
||||||
|
|
||||||
|
if not isinstance(codes, list):
|
||||||
|
raise ValueError("Поле 'codes' должно быть списком")
|
||||||
|
if not isinstance(columns, list):
|
||||||
|
raise ValueError("Поле 'columns' должно быть списком")
|
||||||
|
|
||||||
|
# TODO: Переделать под новую архитектуру
|
||||||
|
return self.get_svodka_total(self.df, codes, columns, search)
|
||||||
|
|
||||||
|
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
|
||||||
|
"""Парсинг файла и возврат DataFrame"""
|
||||||
|
# Сохраняем DataFrame для использования в геттерах
|
||||||
|
self.df = self.parse_svodka_pm_files(file_path, params)
|
||||||
|
return self.df
|
||||||
|
|
||||||
def find_header_row(self, file: str, sheet: str, search_value: str = "Итого", max_rows: int = 50) -> int:
|
def find_header_row(self, file: str, sheet: str, search_value: str = "Итого", max_rows: int = 50) -> int:
|
||||||
"""Определения индекса заголовка в excel по ключевому слову"""
|
"""Определения индекса заголовка в excel по ключевому слову"""
|
||||||
# Читаем первые max_rows строк без заголовков
|
# Читаем первые max_rows строк без заголовков
|
||||||
@@ -16,7 +70,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 +95,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 +117,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 +156,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 +182,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 +212,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 +311,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])
|
||||||
|
|||||||
@@ -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):
|
||||||
"""Интерфейс для хранилища данных"""
|
"""Интерфейс для хранилища данных"""
|
||||||
|
|||||||
@@ -100,8 +100,34 @@ class ReportService:
|
|||||||
# Получаем парсер
|
# Получаем парсер
|
||||||
parser = get_parser(request.report_type)
|
parser = get_parser(request.report_type)
|
||||||
|
|
||||||
# Получаем значение
|
# Устанавливаем DataFrame в парсер для использования в геттерах
|
||||||
value = parser.get_value(df, request.get_params)
|
parser.df = df
|
||||||
|
|
||||||
|
# Получаем параметры запроса
|
||||||
|
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:
|
||||||
|
|||||||
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.
@@ -1,19 +1,28 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Запуск Streamlit интерфейса для NIN Excel Parsers API
|
Запуск Streamlit интерфейса локально из изолированного пакета
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import time
|
import os
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Основная функция"""
|
"""Основная функция"""
|
||||||
print("🚀 ЗАПУСК STREAMLIT ИНТЕРФЕЙСА")
|
print("🚀 ЗАПУСК STREAMLIT ИЗ ИЗОЛИРОВАННОГО ПАКЕТА")
|
||||||
print("=" * 50)
|
print("=" * 60)
|
||||||
print("Убедитесь, что FastAPI сервер запущен на порту 8000")
|
print("Убедитесь, что FastAPI сервер запущен на порту 8000")
|
||||||
print("=" * 50)
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Проверяем, существует ли папка streamlit_app
|
||||||
|
if not os.path.exists("streamlit_app"):
|
||||||
|
print("❌ Папка streamlit_app не найдена")
|
||||||
|
print("Создайте изолированный пакет или используйте docker-compose up -d")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Переходим в папку streamlit_app
|
||||||
|
os.chdir("streamlit_app")
|
||||||
|
|
||||||
# Проверяем, установлен ли Streamlit
|
# Проверяем, установлен ли Streamlit
|
||||||
try:
|
try:
|
||||||
@@ -21,11 +30,12 @@ def main():
|
|||||||
print(f"✅ Streamlit {streamlit.__version__} установлен")
|
print(f"✅ Streamlit {streamlit.__version__} установлен")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("❌ Streamlit не установлен")
|
print("❌ Streamlit не установлен")
|
||||||
print("Установите: pip install streamlit")
|
print("Установите: pip install -r requirements.txt")
|
||||||
return
|
return
|
||||||
|
|
||||||
print("\n🚀 Запускаю Streamlit...")
|
print("\n🚀 Запускаю Streamlit...")
|
||||||
print("📍 URL: http://localhost:8501")
|
print("📍 URL: http://localhost:8501")
|
||||||
|
print("🔗 API: http://localhost:8000")
|
||||||
print("🛑 Для остановки нажмите Ctrl+C")
|
print("🛑 Для остановки нажмите Ctrl+C")
|
||||||
|
|
||||||
# Открываем браузер
|
# Открываем браузер
|
||||||
@@ -35,15 +45,19 @@ def main():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ Не удалось открыть браузер: {e}")
|
print(f"⚠️ Не удалось открыть браузер: {e}")
|
||||||
|
|
||||||
# Запускаем Streamlit
|
# Запускаем Streamlit с правильными переменными окружения
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["DOCKER_ENV"] = "false" # Локальный запуск
|
||||||
|
env["API_BASE_URL"] = "http://localhost:8000" # Локальный API
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.run([
|
subprocess.run([
|
||||||
sys.executable, "-m", "streamlit", "run", "streamlit_app.py",
|
sys.executable, "-m", "streamlit", "run", "app.py",
|
||||||
"--server.port", "8501",
|
"--server.port", "8501",
|
||||||
"--server.address", "localhost",
|
"--server.address", "localhost",
|
||||||
"--server.headless", "false",
|
"--server.headless", "false",
|
||||||
"--browser.gatherUsageStats", "false"
|
"--browser.gatherUsageStats", "false"
|
||||||
])
|
], env=env)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n👋 Streamlit остановлен")
|
print("\n👋 Streamlit остановлен")
|
||||||
|
|
||||||
31
streamlit_app/.dockerignore
Normal file
31
streamlit_app/.dockerignore
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
env
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
.tox
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.log
|
||||||
|
.git
|
||||||
|
.mypy_cache
|
||||||
|
.pytest_cache
|
||||||
|
.hypothesis
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
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/*
|
||||||
|
|
||||||
|
# Копируем файлы зависимостей
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Устанавливаем Python зависимости
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Копируем код приложения
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Открываем порт
|
||||||
|
EXPOSE 8501
|
||||||
|
|
||||||
|
# Команда запуска
|
||||||
|
CMD ["streamlit", "run", "app.py", "--server.port", "8501", "--server.address", "0.0.0.0"]
|
||||||
44
streamlit_app/README.md
Normal file
44
streamlit_app/README.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# 📊 Streamlit App - NIN Excel Parsers API
|
||||||
|
|
||||||
|
Изолированное Streamlit приложение для демонстрации работы NIN Excel Parsers API.
|
||||||
|
|
||||||
|
## 🚀 Запуск
|
||||||
|
|
||||||
|
### Локально:
|
||||||
|
```bash
|
||||||
|
cd streamlit_app
|
||||||
|
pip install -r requirements.txt
|
||||||
|
streamlit run app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### В Docker:
|
||||||
|
```bash
|
||||||
|
docker build -t streamlit-app .
|
||||||
|
docker run -p 8501:8501 streamlit-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Конфигурация
|
||||||
|
|
||||||
|
### Переменные окружения:
|
||||||
|
- `API_BASE_URL` - адрес FastAPI сервера (по умолчанию: `http://fastapi:8000`)
|
||||||
|
|
||||||
|
### Параметры Streamlit:
|
||||||
|
- Порт: 8501
|
||||||
|
- Адрес: 0.0.0.0 (для Docker)
|
||||||
|
- Режим: headless (для Docker)
|
||||||
|
|
||||||
|
## 📁 Структура
|
||||||
|
|
||||||
|
```
|
||||||
|
streamlit_app/
|
||||||
|
├── app.py # Основное приложение
|
||||||
|
├── requirements.txt # Зависимости Python
|
||||||
|
├── Dockerfile # Docker образ
|
||||||
|
├── .streamlit/ # Конфигурация Streamlit
|
||||||
|
│ └── config.toml # Настройки
|
||||||
|
└── README.md # Документация
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Доступ
|
||||||
|
|
||||||
|
После запуска приложение доступно по адресу: **http://localhost:8501**
|
||||||
@@ -15,8 +15,17 @@ st.set_page_config(
|
|||||||
initial_sidebar_state="expanded"
|
initial_sidebar_state="expanded"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Конфигурация API
|
# Конфигурация API - автоматически определяем правильный адрес
|
||||||
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8000")
|
def get_api_base_url():
|
||||||
|
"""Автоматически определяет правильный адрес API"""
|
||||||
|
# Если запущено в Docker, используем внутренний адрес
|
||||||
|
if os.getenv("DOCKER_ENV") == "true":
|
||||||
|
return "http://fastapi:8000"
|
||||||
|
|
||||||
|
# Если запущено локально, используем localhost
|
||||||
|
return "http://localhost:8000"
|
||||||
|
|
||||||
|
API_BASE_URL = os.getenv("API_BASE_URL", get_api_base_url())
|
||||||
|
|
||||||
def check_api_health():
|
def check_api_health():
|
||||||
"""Проверка доступности API"""
|
"""Проверка доступности API"""
|
||||||
@@ -36,6 +45,16 @@ def get_available_parsers():
|
|||||||
except:
|
except:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_parser_getters(parser_name: str):
|
||||||
|
"""Получение информации о геттерах парсера"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/getters")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
return {}
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|
||||||
def get_server_info():
|
def get_server_info():
|
||||||
"""Получение информации о сервере"""
|
"""Получение информации о сервере"""
|
||||||
try:
|
try:
|
||||||
@@ -105,6 +124,9 @@ def main():
|
|||||||
with tab1:
|
with tab1:
|
||||||
st.header("📊 Сводки ПМ - Полный функционал")
|
st.header("📊 Сводки ПМ - Полный функционал")
|
||||||
|
|
||||||
|
# Получаем информацию о геттерах
|
||||||
|
getters_info = get_parser_getters("svodka_pm")
|
||||||
|
|
||||||
# Секция загрузки файлов
|
# Секция загрузки файлов
|
||||||
st.subheader("📤 Загрузка файлов")
|
st.subheader("📤 Загрузка файлов")
|
||||||
uploaded_pm = st.file_uploader(
|
uploaded_pm = st.file_uploader(
|
||||||
@@ -133,6 +155,15 @@ def main():
|
|||||||
# Секция получения данных
|
# Секция получения данных
|
||||||
st.subheader("🔍 Получение данных")
|
st.subheader("🔍 Получение данных")
|
||||||
|
|
||||||
|
# Показываем доступные геттеры
|
||||||
|
if getters_info and "getters" in getters_info:
|
||||||
|
st.info("📋 Доступные геттеры:")
|
||||||
|
for getter_name, getter_info in getters_info["getters"].items():
|
||||||
|
st.write(f"• **{getter_name}**: {getter_info.get('description', 'Нет описания')}")
|
||||||
|
st.write(f" - Обязательные параметры: {', '.join(getter_info.get('required_params', []))}")
|
||||||
|
if getter_info.get('optional_params'):
|
||||||
|
st.write(f" - Необязательные параметры: {', '.join(getter_info['optional_params'])}")
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
@@ -164,12 +195,13 @@ def main():
|
|||||||
if codes and columns:
|
if codes and columns:
|
||||||
with st.spinner("Получаю данные..."):
|
with st.spinner("Получаю данные..."):
|
||||||
data = {
|
data = {
|
||||||
|
"getter": "single_og",
|
||||||
"id": og_id,
|
"id": og_id,
|
||||||
"codes": codes,
|
"codes": codes,
|
||||||
"columns": columns
|
"columns": columns
|
||||||
}
|
}
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_pm/get_single_og", data)
|
result, status = make_api_request("/svodka_pm/get_data", data)
|
||||||
|
|
||||||
if status == 200:
|
if status == 200:
|
||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
@@ -200,11 +232,12 @@ def main():
|
|||||||
if codes_total and columns_total:
|
if codes_total and columns_total:
|
||||||
with st.spinner("Получаю данные..."):
|
with st.spinner("Получаю данные..."):
|
||||||
data = {
|
data = {
|
||||||
|
"getter": "total_ogs",
|
||||||
"codes": codes_total,
|
"codes": codes_total,
|
||||||
"columns": columns_total
|
"columns": columns_total
|
||||||
}
|
}
|
||||||
|
|
||||||
result, status = make_api_request("/svodka_pm/get_total_ogs", data)
|
result, status = make_api_request("/svodka_pm/get_data", data)
|
||||||
|
|
||||||
if status == 200:
|
if status == 200:
|
||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
@@ -218,6 +251,9 @@ def main():
|
|||||||
with tab2:
|
with tab2:
|
||||||
st.header("🏭 Сводки СА - Полный функционал")
|
st.header("🏭 Сводки СА - Полный функционал")
|
||||||
|
|
||||||
|
# Получаем информацию о геттерах
|
||||||
|
getters_info = get_parser_getters("svodka_ca")
|
||||||
|
|
||||||
# Секция загрузки файлов
|
# Секция загрузки файлов
|
||||||
st.subheader("📤 Загрузка файлов")
|
st.subheader("📤 Загрузка файлов")
|
||||||
uploaded_ca = st.file_uploader(
|
uploaded_ca = st.file_uploader(
|
||||||
@@ -245,7 +281,16 @@ def main():
|
|||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
# Секция получения данных
|
# Секция получения данных
|
||||||
st.subheader("🔍 Получение данных")
|
st.subheader("<EFBFBD><EFBFBD> Получение данных")
|
||||||
|
|
||||||
|
# Показываем доступные геттеры
|
||||||
|
if getters_info and "getters" in getters_info:
|
||||||
|
st.info("📋 Доступные геттеры:")
|
||||||
|
for getter_name, getter_info in getters_info["getters"].items():
|
||||||
|
st.write(f"• **{getter_name}**: {getter_info.get('description', 'Нет описания')}")
|
||||||
|
st.write(f" - Обязательные параметры: {', '.join(getter_info.get('required_params', []))}")
|
||||||
|
if getter_info.get('optional_params'):
|
||||||
|
st.write(f" - Необязательные параметры: {', '.join(getter_info['optional_params'])}")
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
@@ -254,8 +299,8 @@ def main():
|
|||||||
|
|
||||||
modes = st.multiselect(
|
modes = st.multiselect(
|
||||||
"Выберите режимы",
|
"Выберите режимы",
|
||||||
["План", "Факт", "Норматив"],
|
["plan", "fact", "normativ"],
|
||||||
default=["План", "Факт"],
|
default=["plan", "fact"],
|
||||||
key="ca_modes"
|
key="ca_modes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -272,6 +317,7 @@ def main():
|
|||||||
if modes and tables:
|
if modes and tables:
|
||||||
with st.spinner("Получаю данные..."):
|
with st.spinner("Получаю данные..."):
|
||||||
data = {
|
data = {
|
||||||
|
"getter": "get_data",
|
||||||
"modes": modes,
|
"modes": modes,
|
||||||
"tables": tables
|
"tables": tables
|
||||||
}
|
}
|
||||||
@@ -282,7 +328,7 @@ def main():
|
|||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
st.json(result)
|
st.json(result)
|
||||||
else:
|
else:
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
st.error(f"❌ Ошибка: {result.get('message', f'Неизвестная ошибка: {status}')}")
|
||||||
else:
|
else:
|
||||||
st.warning("⚠️ Выберите режимы и таблицы")
|
st.warning("⚠️ Выберите режимы и таблицы")
|
||||||
|
|
||||||
@@ -290,6 +336,9 @@ def main():
|
|||||||
with tab3:
|
with tab3:
|
||||||
st.header("⛽ Мониторинг топлива - Полный функционал")
|
st.header("⛽ Мониторинг топлива - Полный функционал")
|
||||||
|
|
||||||
|
# Получаем информацию о геттерах
|
||||||
|
getters_info = get_parser_getters("monitoring_fuel")
|
||||||
|
|
||||||
# Секция загрузки файлов
|
# Секция загрузки файлов
|
||||||
st.subheader("📤 Загрузка файлов")
|
st.subheader("📤 Загрузка файлов")
|
||||||
uploaded_fuel = st.file_uploader(
|
uploaded_fuel = st.file_uploader(
|
||||||
@@ -318,6 +367,15 @@ def main():
|
|||||||
# Секция получения данных
|
# Секция получения данных
|
||||||
st.subheader("🔍 Получение данных")
|
st.subheader("🔍 Получение данных")
|
||||||
|
|
||||||
|
# Показываем доступные геттеры
|
||||||
|
if getters_info and "getters" in getters_info:
|
||||||
|
st.info("📋 Доступные геттеры:")
|
||||||
|
for getter_name, getter_info in getters_info["getters"].items():
|
||||||
|
st.write(f"• **{getter_name}**: {getter_info.get('description', 'Нет описания')}")
|
||||||
|
st.write(f" - Обязательные параметры: {', '.join(getter_info.get('required_params', []))}")
|
||||||
|
if getter_info.get('optional_params'):
|
||||||
|
st.write(f" - Необязательные параметры: {', '.join(getter_info['optional_params'])}")
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
@@ -334,10 +392,11 @@ def main():
|
|||||||
if columns_fuel:
|
if columns_fuel:
|
||||||
with st.spinner("Получаю данные..."):
|
with st.spinner("Получаю данные..."):
|
||||||
data = {
|
data = {
|
||||||
|
"getter": "total_by_columns",
|
||||||
"columns": columns_fuel
|
"columns": columns_fuel
|
||||||
}
|
}
|
||||||
|
|
||||||
result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data)
|
result, status = make_api_request("/monitoring_fuel/get_data", data)
|
||||||
|
|
||||||
if status == 200:
|
if status == 200:
|
||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
@@ -359,10 +418,11 @@ def main():
|
|||||||
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
|
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
|
||||||
with st.spinner("Получаю данные..."):
|
with st.spinner("Получаю данные..."):
|
||||||
data = {
|
data = {
|
||||||
|
"getter": "month_by_code",
|
||||||
"month": month
|
"month": month
|
||||||
}
|
}
|
||||||
|
|
||||||
result, status = make_api_request("/monitoring_fuel/get_month_by_code", data)
|
result, status = make_api_request("/monitoring_fuel/get_data", data)
|
||||||
|
|
||||||
if status == 200:
|
if status == 200:
|
||||||
st.success("✅ Данные получены")
|
st.success("✅ Данные получены")
|
||||||
4
streamlit_app/requirements.txt
Normal file
4
streamlit_app/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
streamlit>=1.28.0
|
||||||
|
requests>=2.31.0
|
||||||
|
pandas>=1.5.0
|
||||||
|
numpy>=1.24.0
|
||||||
84
test_api.py
Normal file
84
test_api.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тестовый скрипт для проверки API
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
def test_api_endpoints():
|
||||||
|
"""Тестирование API эндпоинтов"""
|
||||||
|
base_url = "http://localhost:8000"
|
||||||
|
|
||||||
|
print("🧪 ТЕСТИРОВАНИЕ API")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Тест 1: Проверка доступности API
|
||||||
|
print("\n1️⃣ Проверка доступности API...")
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{base_url}/")
|
||||||
|
if response.status_code == 200:
|
||||||
|
print(f"✅ API доступен: {response.json()}")
|
||||||
|
else:
|
||||||
|
print(f"❌ API недоступен: {response.status_code}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка подключения к API: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Тест 2: Список парсеров
|
||||||
|
print("\n2️⃣ Получение списка парсеров...")
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{base_url}/parsers")
|
||||||
|
if response.status_code == 200:
|
||||||
|
parsers = response.json()
|
||||||
|
print(f"✅ Парсеры: {parsers}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка получения парсеров: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка: {e}")
|
||||||
|
|
||||||
|
# Тест 3: Информация о геттерах
|
||||||
|
print("\n3️⃣ Информация о геттерах парсеров...")
|
||||||
|
parsers_to_test = ["svodka_pm", "svodka_ca", "monitoring_fuel"]
|
||||||
|
|
||||||
|
for parser in parsers_to_test:
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{base_url}/parsers/{parser}/getters")
|
||||||
|
if response.status_code == 200:
|
||||||
|
getters = response.json()
|
||||||
|
print(f"✅ {parser}: {len(getters.get('getters', {}))} геттеров")
|
||||||
|
else:
|
||||||
|
print(f"❌ {parser}: ошибка {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ {parser}: ошибка {e}")
|
||||||
|
|
||||||
|
# Тест 4: Загрузка тестового файла
|
||||||
|
print("\n4️⃣ Тест загрузки файла...")
|
||||||
|
try:
|
||||||
|
# Создаем простой Excel файл для теста
|
||||||
|
test_data = b"test content"
|
||||||
|
files = {"file": ("test.xlsx", test_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
|
||||||
|
|
||||||
|
response = requests.post(f"{base_url}/svodka_ca/upload", files=files)
|
||||||
|
print(f"📤 Результат загрузки: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
print(f"✅ Файл загружен: {result}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка загрузки: {response.status_code}")
|
||||||
|
try:
|
||||||
|
error_detail = response.json()
|
||||||
|
print(f"📋 Детали ошибки: {error_detail}")
|
||||||
|
except:
|
||||||
|
print(f"📋 Текст ошибки: {response.text}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка теста загрузки: {e}")
|
||||||
|
|
||||||
|
print("\n🎯 Тестирование завершено!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_api_endpoints()
|
||||||
79
test_api_direct.py
Normal file
79
test_api_direct.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Прямое тестирование API эндпоинтов
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
def test_api_endpoints():
|
||||||
|
"""Тестирование API эндпоинтов"""
|
||||||
|
base_url = "http://localhost:8000"
|
||||||
|
|
||||||
|
print("🧪 ПРЯМОЕ ТЕСТИРОВАНИЕ API")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
# Тест 1: Проверка доступности API
|
||||||
|
print("\n1️⃣ Проверка доступности API...")
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{base_url}/")
|
||||||
|
print(f"✅ API доступен: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Тест 2: Тестирование эндпоинта svodka_ca/get_data
|
||||||
|
print("\n2️⃣ Тестирование svodka_ca/get_data...")
|
||||||
|
try:
|
||||||
|
data = {
|
||||||
|
"getter": "get_data",
|
||||||
|
"modes": ["plan", "fact"],
|
||||||
|
"tables": ["ТиП", "Топливо"]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(f"{base_url}/svodka_ca/get_data", json=data)
|
||||||
|
print(f"📥 Результат: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
print(f"✅ Успешно: {result}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
error_detail = response.json()
|
||||||
|
print(f"❌ Ошибка: {error_detail}")
|
||||||
|
except:
|
||||||
|
print(f"❌ Ошибка: {response.text}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Исключение: {e}")
|
||||||
|
|
||||||
|
# Тест 3: Тестирование эндпоинта svodka_pm/get_data
|
||||||
|
print("\n3️⃣ Тестирование svodka_pm/get_data...")
|
||||||
|
try:
|
||||||
|
data = {
|
||||||
|
"getter": "single_og",
|
||||||
|
"id": "SNPZ",
|
||||||
|
"codes": [78, 79],
|
||||||
|
"columns": ["БП", "ПП"]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(f"{base_url}/svodka_pm/get_data", json=data)
|
||||||
|
print(f"📥 Результат: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
print(f"✅ Успешно: {result}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
error_detail = response.json()
|
||||||
|
print(f"❌ Ошибка: {error_detail}")
|
||||||
|
except:
|
||||||
|
print(f"❌ Ошибка: {response.text}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Исключение: {e}")
|
||||||
|
|
||||||
|
print("\n🎯 Тестирование завершено!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_api_endpoints()
|
||||||
96
test_ca_workflow.py
Normal file
96
test_ca_workflow.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тестирование полного workflow с сводкой СА
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
def test_ca_workflow():
|
||||||
|
"""Тестирование полного workflow с сводкой СА"""
|
||||||
|
base_url = "http://localhost:8000"
|
||||||
|
test_file = "python_parser/data/svodka_ca.xlsx"
|
||||||
|
|
||||||
|
print("🧪 ТЕСТ ПОЛНОГО WORKFLOW СВОДКИ СА")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Проверяем, что файл существует
|
||||||
|
if not os.path.exists(test_file):
|
||||||
|
print(f"❌ Файл {test_file} не найден")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"📁 Тестовый файл найден: {test_file}")
|
||||||
|
print(f"📏 Размер: {os.path.getsize(test_file)} байт")
|
||||||
|
|
||||||
|
# Шаг 1: Загружаем файл
|
||||||
|
print("\n1️⃣ Загружаю файл сводки СА...")
|
||||||
|
try:
|
||||||
|
with open(test_file, 'rb') as f:
|
||||||
|
file_data = f.read()
|
||||||
|
|
||||||
|
files = {"file": ("svodka_ca.xlsx", file_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
|
||||||
|
|
||||||
|
response = requests.post(f"{base_url}/svodka_ca/upload", files=files)
|
||||||
|
print(f"📤 Результат загрузки: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
print(f"✅ Файл загружен: {result}")
|
||||||
|
object_id = result.get('object_id', 'nin_excel_data_svodka_ca')
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка загрузки: {response.status_code}")
|
||||||
|
try:
|
||||||
|
error_detail = response.json()
|
||||||
|
print(f"📋 Детали ошибки: {error_detail}")
|
||||||
|
except:
|
||||||
|
print(f"📋 Текст ошибки: {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка загрузки: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Шаг 2: Получаем данные через геттер
|
||||||
|
print("\n2️⃣ Получаю данные через геттер...")
|
||||||
|
try:
|
||||||
|
data = {
|
||||||
|
"getter": "get_data",
|
||||||
|
"modes": ["plan", "fact"], # Используем английские названия
|
||||||
|
"tables": ["ТиП", "Топливо"]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(f"{base_url}/svodka_ca/get_data", json=data)
|
||||||
|
print(f"📥 Результат получения данных: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
print(f"✅ Данные получены успешно!")
|
||||||
|
print(f"📊 Размер ответа: {len(str(result))} символов")
|
||||||
|
|
||||||
|
# Показываем структуру данных
|
||||||
|
if isinstance(result, dict):
|
||||||
|
print(f"🔍 Структура данных:")
|
||||||
|
for key, value in result.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
print(f" {key}: {len(value)} элементов")
|
||||||
|
else:
|
||||||
|
print(f" {key}: {type(value).__name__}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Ошибка получения данных: {response.status_code}")
|
||||||
|
try:
|
||||||
|
error_detail = response.json()
|
||||||
|
print(f"📋 Детали ошибки: {error_detail}")
|
||||||
|
except:
|
||||||
|
print(f"📋 Текст ошибки: {response.text}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка получения данных: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("\n🎯 Тестирование завершено успешно!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_ca_workflow()
|
||||||
110
test_minio_connection.py
Normal file
110
test_minio_connection.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тестовый скрипт для проверки подключения к MinIO
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
from minio import Minio
|
||||||
|
|
||||||
|
def test_minio_connection():
|
||||||
|
"""Тестирование подключения к MinIO"""
|
||||||
|
print("🔍 Тестирование подключения к MinIO...")
|
||||||
|
|
||||||
|
# Параметры подключения
|
||||||
|
endpoint = os.getenv("MINIO_ENDPOINT", "localhost:9000")
|
||||||
|
access_key = os.getenv("MINIO_ACCESS_KEY", "minioadmin")
|
||||||
|
secret_key = os.getenv("MINIO_SECRET_KEY", "minioadmin")
|
||||||
|
bucket_name = os.getenv("MINIO_BUCKET", "svodka-data")
|
||||||
|
|
||||||
|
print(f"📍 Endpoint: {endpoint}")
|
||||||
|
print(f"🔑 Access Key: {access_key}")
|
||||||
|
print(f"🔐 Secret Key: {secret_key}")
|
||||||
|
print(f"🪣 Bucket: {bucket_name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем клиент
|
||||||
|
print("\n🚀 Создаю MinIO клиент...")
|
||||||
|
client = Minio(
|
||||||
|
endpoint,
|
||||||
|
access_key=access_key,
|
||||||
|
secret_key=secret_key,
|
||||||
|
secure=False,
|
||||||
|
cert_check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем подключение
|
||||||
|
print("✅ MinIO клиент создан")
|
||||||
|
|
||||||
|
# Проверяем bucket
|
||||||
|
print(f"\n🔍 Проверяю bucket '{bucket_name}'...")
|
||||||
|
if client.bucket_exists(bucket_name):
|
||||||
|
print(f"✅ Bucket '{bucket_name}' существует")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ Bucket '{bucket_name}' не существует, создаю...")
|
||||||
|
client.make_bucket(bucket_name)
|
||||||
|
print(f"✅ Bucket '{bucket_name}' создан")
|
||||||
|
|
||||||
|
# Пробуем загрузить тестовый файл
|
||||||
|
print("\n📤 Тестирую загрузку файла...")
|
||||||
|
test_data = b"Hello MinIO!"
|
||||||
|
test_stream = io.BytesIO(test_data)
|
||||||
|
|
||||||
|
client.put_object(
|
||||||
|
bucket_name,
|
||||||
|
"test.txt",
|
||||||
|
test_stream,
|
||||||
|
length=len(test_data),
|
||||||
|
content_type='text/plain'
|
||||||
|
)
|
||||||
|
print("✅ Тестовый файл загружен")
|
||||||
|
|
||||||
|
# Пробуем скачать файл
|
||||||
|
print("\n📥 Тестирую скачивание файла...")
|
||||||
|
response = client.get_object(bucket_name, "test.txt")
|
||||||
|
downloaded_data = response.read()
|
||||||
|
print(f"✅ Файл скачан: {downloaded_data}")
|
||||||
|
|
||||||
|
# Удаляем тестовый файл
|
||||||
|
client.remove_object(bucket_name, "test.txt")
|
||||||
|
print("✅ Тестовый файл удален")
|
||||||
|
|
||||||
|
print("\n🎉 Все тесты MinIO прошли успешно!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Ошибка подключения к MinIO: {e}")
|
||||||
|
print(f"Тип ошибки: {type(e).__name__}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_environment():
|
||||||
|
"""Проверка переменных окружения"""
|
||||||
|
print("🔧 Проверка переменных окружения:")
|
||||||
|
env_vars = [
|
||||||
|
"MINIO_ENDPOINT",
|
||||||
|
"MINIO_ACCESS_KEY",
|
||||||
|
"MINIO_SECRET_KEY",
|
||||||
|
"MINIO_BUCKET"
|
||||||
|
]
|
||||||
|
|
||||||
|
for var in env_vars:
|
||||||
|
value = os.getenv(var, "НЕ УСТАНОВЛЕНО")
|
||||||
|
print(f" {var}: {value}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=" * 60)
|
||||||
|
print("🧪 ТЕСТ ПОДКЛЮЧЕНИЯ К MINIO")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
test_environment()
|
||||||
|
print()
|
||||||
|
|
||||||
|
success = test_minio_connection()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n✅ MinIO работает корректно!")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("\n❌ Проблемы с MinIO!")
|
||||||
|
sys.exit(1)
|
||||||
69
test_upload.py
Normal file
69
test_upload.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тестирование загрузки Excel файла
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
def test_file_upload():
|
||||||
|
"""Тестирование загрузки файла"""
|
||||||
|
base_url = "http://localhost:8000"
|
||||||
|
filename = "test_file.xlsx"
|
||||||
|
|
||||||
|
print("🧪 ТЕСТ ЗАГРУЗКИ ФАЙЛА")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
# Проверяем, что файл существует
|
||||||
|
if not os.path.exists(filename):
|
||||||
|
print(f"❌ Файл {filename} не найден")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"📁 Файл найден: {filename}")
|
||||||
|
print(f"📏 Размер: {os.path.getsize(filename)} байт")
|
||||||
|
|
||||||
|
# Тестируем загрузку в разные парсеры
|
||||||
|
parsers = [
|
||||||
|
("svodka_ca", "/svodka_ca/upload", "file"),
|
||||||
|
("monitoring_fuel", "/monitoring_fuel/upload-zip", "zip_file"),
|
||||||
|
("svodka_pm", "/svodka_pm/upload-zip", "zip_file")
|
||||||
|
]
|
||||||
|
|
||||||
|
for parser_name, endpoint, file_param in parsers:
|
||||||
|
print(f"\n🔍 Тестирую {parser_name}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Читаем файл
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
file_data = f.read()
|
||||||
|
|
||||||
|
# Определяем content type
|
||||||
|
if filename.endswith('.xlsx'):
|
||||||
|
content_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
else:
|
||||||
|
content_type = "application/octet-stream"
|
||||||
|
|
||||||
|
# Загружаем файл с правильным параметром
|
||||||
|
files = {file_param: (filename, file_data, content_type)}
|
||||||
|
|
||||||
|
response = requests.post(f"{base_url}{endpoint}", files=files)
|
||||||
|
print(f"📤 Результат: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
print(f"✅ Успешно: {result}")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
error_detail = response.json()
|
||||||
|
print(f"❌ Ошибка: {error_detail}")
|
||||||
|
except:
|
||||||
|
print(f"❌ Ошибка: {response.text}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Исключение: {e}")
|
||||||
|
|
||||||
|
print("\n🎯 Тестирование завершено!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_file_upload()
|
||||||
Reference in New Issue
Block a user