This commit is contained in:
2025-08-28 12:16:36 +03:00
parent 3b238ae283
commit f16ab7133a
48 changed files with 1221 additions and 46 deletions

View File

@@ -0,0 +1,28 @@
[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

View File

@@ -1,20 +1,23 @@
FROM repo-dev.predix.rosneft.ru/python:3.11-slim
FROM python:3.11-slim
WORKDIR /app
# RUN pip install kafka-python==2.0.2
# RUN pip freeze > /app/requirements.txt
# Устанавливаем системные зависимости
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# ADD . /app
# Копируем файлы зависимостей
COPY requirements.txt .
RUN mkdir -p vendor
RUN pip download -r /app/requirements.txt --no-binary=:none: -d /app/vendor
# Устанавливаем Python зависимости
RUN pip install --no-cache-dir -r requirements.txt
# ADD . /app
# Копируем код приложения
COPY . .
# 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
# Открываем порт
EXPOSE 8000
# CMD ["python", "/app/run_dev.py"]
# Команда запуска
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -0,0 +1,66 @@
# 🚀 Быстрый старт 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
```

197
python_parser/README.md Normal file
View File

@@ -0,0 +1,197 @@
# NIN Excel Parsers API
API для парсинга Excel отчетов нефтеперерабатывающих заводов (НПЗ) с использованием FastAPI и MinIO для хранения данных.
## 🚀 Быстрый запуск
### **Вариант 1: Только MinIO в Docker + FastAPI локально**
```bash
# Запуск MinIO в Docker
docker-compose up -d minio
# Запуск FastAPI локально
python run_dev.py
# В отдельном терминале запуск Streamlit
python run_streamlit.py
```
### **Вариант 2: MinIO + FastAPI в Docker + Streamlit локально**
```bash
# Запуск MinIO и FastAPI в Docker
docker-compose up -d
# В отдельном терминале запуск Streamlit
python run_streamlit.py
```
### **Вариант 3: Только MinIO в Docker**
```bash
# Запуск только MinIO
docker-compose up -d minio
```
## 📋 Описание сервисов
- **MinIO** (порт 9000-9001): S3-совместимое хранилище для данных
- **FastAPI** (порт 8000): API сервер для парсинга Excel файлов
- **Streamlit** (порт 8501): Веб-интерфейс для демонстрации API
## 🔧 Диагностика
Для проверки состояния всех сервисов:
```bash
python check_services.py
```
## 🛑 Остановка
### Остановка Docker сервисов:
```bash
# Все сервисы
docker-compose down
# Только MinIO
docker-compose stop minio
```
### Остановка Streamlit:
```bash
# Нажмите Ctrl+C в терминале с Streamlit
```
## 📁 Структура проекта
```
python_parser/
├── app/ # FastAPI приложение
│ ├── main.py # Основной файл приложения
│ └── schemas/ # Pydantic схемы
├── core/ # Бизнес-логика
│ ├── models.py # Модели данных
│ ├── ports.py # Интерфейсы (порты)
│ └── services.py # Сервисы
├── adapters/ # Адаптеры для внешних систем
│ ├── storage.py # MinIO адаптер
│ └── parsers/ # Парсеры Excel файлов
├── data/ # Тестовые данные
├── docker-compose.yml # Docker Compose конфигурация
├── Dockerfile # Docker образ для FastAPI
├── run_dev.py # Запуск FastAPI локально
├── run_streamlit.py # Запуск Streamlit
└── check_services.py # Диагностика сервисов
```
## 🔍 Доступные эндпоинты
- **GET /** - Информация об API
- **GET /docs** - Swagger документация
- **POST /svodka_pm/upload-zip** - Загрузка сводок ПМ
- **POST /svodka_ca/upload-zip** - Загрузка сводок ЦА
- **POST /monitoring_fuel/upload-zip** - Загрузка мониторинга топлива
- **GET /svodka_pm/data** - Получение данных сводок ПМ
- **GET /svodka_ca/data** - Получение данных сводок ЦА
- **GET /monitoring_fuel/data** - Получение данных мониторинга топлива
## 📊 Поддерживаемые типы отчетов
1. **svodka_pm** - Сводки по переработке нефти (ПМ)
2. **svodka_ca** - Сводки по переработке нефти (ЦА)
3. **monitoring_fuel** - Мониторинг топлива
## 🐳 Docker команды
### Сборка и запуск:
```bash
# Все сервисы
docker-compose up -d --build
# Только MinIO
docker-compose up -d minio
# Только FastAPI (требует MinIO)
docker-compose up -d fastapi
```
### Просмотр логов:
```bash
# Все сервисы
docker-compose logs
# Конкретный сервис
docker-compose logs fastapi
docker-compose logs minio
```
### Остановка:
```bash
docker-compose down
```
## 🔧 Устранение неполадок
### Проблема: "Streamlit не может подключиться к FastAPI"
**Симптомы:**
- Streamlit открывается, но показывает "API недоступен по адресу http://localhost:8000"
- FastAPI не отвечает на порту 8000
**Решения:**
1. **Проверьте порты:**
```bash
# Windows
netstat -an | findstr :8000
# Linux/Mac
netstat -an | grep :8000
```
2. **Перезапустите FastAPI:**
```bash
# Остановите текущий процесс (Ctrl+C)
python run_dev.py
```
3. **Проверьте логи Docker:**
```bash
docker-compose logs fastapi
```
### Проблема: "MinIO недоступен"
**Решения:**
1. Запустите Docker Desktop
2. Проверьте статус контейнера: `docker ps`
3. Перезапустите MinIO: `docker-compose restart minio`
### Проблема: "Порт уже занят"
**Решения:**
1. Найдите процесс: `netstat -ano | findstr :8000`
2. Остановите процесс: `taskkill /PID <номер_процесса>`
3. Или используйте другой порт в конфигурации
## 🚀 Разработка
### Добавление нового парсера:
1. Создайте файл в `adapters/parsers/`
2. Реализуйте интерфейс `ParserPort`
3. Добавьте в `core/services.py`
4. Создайте схемы в `app/schemas/`
5. Добавьте эндпоинты в `app/main.py`
### Тестирование:
```bash
# Запуск тестов
pytest
# Запуск с покрытием
pytest --cov=.
```
## 📝 Лицензия
Проект разработан для внутреннего использования НИН.

View File

@@ -0,0 +1,186 @@
# 🚀 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

View File

@@ -15,24 +15,53 @@ class MinIOStorageAdapter(StoragePort):
"""Адаптер для MinIO хранилища"""
def __init__(self):
self.client = Minio(
os.getenv("MINIO_ENDPOINT", "localhost:9000"),
access_key=os.getenv("MINIO_ACCESS_KEY", "minioadmin"),
secret_key=os.getenv("MINIO_SECRET_KEY", "minioadmin"),
secure=os.getenv("MINIO_SECURE", "false").lower() == "true",
cert_check=False
)
self.bucket_name = os.getenv("MINIO_BUCKET", "svodka-data")
self._ensure_bucket_exists()
self._client = None
self._bucket_name = os.getenv("MINIO_BUCKET", "svodka-data")
self._endpoint = os.getenv("MINIO_ENDPOINT", "localhost:9000")
self._access_key = os.getenv("MINIO_ACCESS_KEY", "minioadmin")
self._secret_key = os.getenv("MINIO_SECRET_KEY", "minioadmin")
self._secure = os.getenv("MINIO_SECURE", "false").lower() == "true"
@property
def client(self):
"""Ленивая инициализация MinIO клиента"""
if self._client is None:
try:
self._client = Minio(
self._endpoint,
access_key=self._access_key,
secret_key=self._secret_key,
secure=self._secure,
cert_check=False
)
# Проверяем bucket только при первом использовании
self._ensure_bucket_exists()
except Exception as e:
print(f"⚠️ Не удалось подключиться к MinIO: {e}")
print("MinIO будет недоступен, но приложение продолжит работать")
return None
return self._client
def _ensure_bucket_exists(self):
"""Проверка существования bucket и создание при необходимости"""
if not self.client.bucket_exists(self.bucket_name):
self.client.make_bucket(self.bucket_name)
print(f"Bucket '{self.bucket_name}' создан")
if self.client is None:
return False
try:
if not self.client.bucket_exists(self._bucket_name):
self.client.make_bucket(self._bucket_name)
print(f"✅ Bucket '{self._bucket_name}' создан")
return True
except Exception as e:
print(f"❌ Ошибка при работе с bucket: {e}")
return False
def save_dataframe(self, df: pd.DataFrame, object_id: str) -> bool:
"""Сохранение DataFrame в MinIO"""
if self.client is None:
print("⚠️ MinIO недоступен, данные не сохранены")
return False
try:
# Сериализуем DataFrame
data = pickle.dumps(df)
@@ -42,24 +71,28 @@ class MinIOStorageAdapter(StoragePort):
# Загружаем в MinIO
self.client.put_object(
self.bucket_name,
self._bucket_name,
object_id,
data_stream,
length=len(data),
content_type='application/octet-stream'
)
print(f"DataFrame успешно сохранен в MinIO: {self.bucket_name}/{object_id}")
print(f"DataFrame успешно сохранен в MinIO: {self._bucket_name}/{object_id}")
return True
except Exception as e:
print(f"Ошибка при сохранении в MinIO: {e}")
print(f"Ошибка при сохранении в MinIO: {e}")
return False
def load_dataframe(self, object_id: str) -> Optional[pd.DataFrame]:
"""Загрузка DataFrame из MinIO"""
if self.client is None:
print("⚠️ MinIO недоступен, данные не загружены")
return None
try:
# Получаем объект из MinIO
response = self.client.get_object(self.bucket_name, object_id)
response = self.client.get_object(self._bucket_name, object_id)
# Читаем данные
data = response.read()
@@ -69,7 +102,7 @@ class MinIOStorageAdapter(StoragePort):
return df
except Exception as e:
print(f"Ошибка при загрузке данных из MinIO: {e}")
print(f"Ошибка при загрузке данных из MinIO: {e}")
return None
finally:
if 'response' in locals():
@@ -78,18 +111,25 @@ class MinIOStorageAdapter(StoragePort):
def delete_object(self, object_id: str) -> bool:
"""Удаление объекта из MinIO"""
if self.client is None:
print("⚠️ MinIO недоступен, объект не удален")
return False
try:
self.client.remove_object(self.bucket_name, object_id)
print(f"Объект успешно удален из MinIO: {self.bucket_name}/{object_id}")
self.client.remove_object(self._bucket_name, object_id)
print(f"Объект успешно удален из MinIO: {self._bucket_name}/{object_id}")
return True
except Exception as e:
print(f"Ошибка при удалении объекта из MinIO: {e}")
print(f"Ошибка при удалении объекта из MinIO: {e}")
return False
def object_exists(self, object_id: str) -> bool:
"""Проверка существования объекта в MinIO"""
if self.client is None:
return False
try:
self.client.stat_object(self.bucket_name, object_id)
self.client.stat_object(self._bucket_name, object_id)
return True
except Exception:
return False

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""
Скрипт для быстрой диагностики всех сервисов NIN Excel Parsers
"""
import requests
import subprocess
import sys
from pathlib import Path
def check_port(port, service_name):
"""Проверка доступности порта"""
try:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex(('localhost', port))
sock.close()
if result == 0:
print(f"✅ Порт {port} ({service_name}) - ОТКРЫТ")
return True
else:
print(f"❌ Порт {port} ({service_name}) - ЗАКРЫТ")
return False
except Exception as e:
print(f"❌ Ошибка проверки порта {port}: {e}")
return False
def check_service(url, service_name):
"""Проверка доступности HTTP сервиса"""
try:
response = requests.get(url, timeout=3)
if response.status_code == 200:
print(f"{service_name} ({url}) - ДОСТУПЕН")
return True
else:
print(f"⚠️ {service_name} ({url}) - ОТВЕЧАЕТ, но статус {response.status_code}")
return True
except requests.exceptions.ConnectionError:
print(f"{service_name} ({url}) - НЕ ДОСТУПЕН (Connection Error)")
return False
except requests.exceptions.Timeout:
print(f"⚠️ {service_name} ({url}) - ТАЙМАУТ")
return False
except Exception as e:
print(f"{service_name} ({url}) - ОШИБКА: {e}")
return False
def check_docker():
"""Проверка Docker"""
try:
result = subprocess.run(["docker", "--version"], capture_output=True, text=True)
if result.returncode == 0:
print(f"✅ Docker: {result.stdout.strip()}")
return True
else:
print("❌ Docker не работает")
return False
except FileNotFoundError:
print("❌ Docker не установлен")
return False
except Exception as e:
print(f"❌ Ошибка проверки Docker: {e}")
return False
def check_docker_containers():
"""Проверка Docker контейнеров"""
try:
result = subprocess.run(["docker", "ps"], capture_output=True, text=True)
if result.returncode == 0:
if "minio" in result.stdout.lower():
print("✅ MinIO контейнер запущен")
return True
else:
print("⚠️ MinIO контейнер не найден")
return False
else:
print("Не удалось проверить Docker контейнеры")
return False
except Exception as e:
print(f"❌ Ошибка проверки контейнеров: {e}")
return False
def check_python_packages():
"""Проверка Python пакетов"""
required_packages = ['fastapi', 'streamlit', 'pandas', 'minio', 'uvicorn']
missing_packages = []
print("\n🔍 Проверка Python пакетов:")
for package in required_packages:
try:
__import__(package)
print(f"{package}")
except ImportError:
print(f"{package}")
missing_packages.append(package)
if missing_packages:
print(f"\n⚠️ Отсутствуют пакеты: {', '.join(missing_packages)}")
print("Установите: pip install -r requirements.txt")
return False
return True
def main():
"""Основная функция диагностики"""
print("🔍 ДИАГНОСТИКА NIN Excel Parsers API")
print("=" * 50)
# Проверка Python пакетов
packages_ok = check_python_packages()
print("\n🔍 Проверка Docker:")
docker_ok = check_docker()
if docker_ok:
containers_ok = check_docker_containers()
else:
containers_ok = False
print("\n🔍 Проверка портов:")
port_8000_ok = check_port(8000, "FastAPI")
port_8501_ok = check_port(8501, "Streamlit")
port_9000_ok = check_port(9000, "MinIO API")
port_9001_ok = check_port(9001, "MinIO Console")
print("\n🔍 Проверка HTTP сервисов:")
fastapi_ok = check_service("http://localhost:8000/", "FastAPI")
streamlit_ok = check_service("http://localhost:8501/", "Streamlit")
minio_console_ok = check_service("http://localhost:9001/", "MinIO Console")
print("\n" + "=" * 50)
print("📊 РЕЗУЛЬТАТЫ ДИАГНОСТИКИ:")
print("=" * 50)
# Подсчет результатов
total_checks = 8
passed_checks = sum([
packages_ok,
docker_ok,
containers_ok,
port_8000_ok,
port_8501_ok,
port_9000_ok,
port_9001_ok,
fastapi_ok
])
print(f"✅ Пройдено: {passed_checks}/{total_checks}")
if passed_checks == total_checks:
print("\n🎉 Все сервисы работают корректно!")
print("📍 Доступные URL:")
print(" • Streamlit: http://localhost:8501")
print(" • FastAPI: http://localhost:8000")
print(" • API Docs: http://localhost:8000/docs")
print(" • MinIO: http://localhost:9001")
else:
print(f"\n⚠️ Проблемы обнаружены в {total_checks - passed_checks} сервисах")
if not packages_ok:
print("\n🔧 РЕШЕНИЕ: Установите зависимости")
print("pip install -r requirements.txt")
if not docker_ok:
print("\n🔧 РЕШЕНИЕ: Запустите Docker Desktop")
if not containers_ok:
print("\n🔧 РЕШЕНИЕ: Запустите MinIO")
print("docker-compose up -d minio")
if not port_8000_ok:
print("\n🔧 РЕШЕНИЕ: Запустите FastAPI сервер")
print("python run_dev.py")
if not port_8501_ok:
print("\n🔧 РЕШЕНИЕ: Запустите Streamlit")
print("python run_streamlit.py")
print("\n🚀 Для автоматического запуска используйте:")
print("python start_demo.py")
print("\n🔍 Для пошагового запуска используйте:")
print("python run_manual.py")
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

View File

@@ -48,7 +48,7 @@ class ReportService:
# Генерируем object_id
object_id = f"nin_excel_data_{request.report_type}"
# Удаляем старый объект, если он существует
# Удаляем старый объект, если он существует и хранилище доступно
if self.storage.object_exists(object_id):
self.storage.delete_object(object_id)
print(f"Старый объект удален: {object_id}")
@@ -63,7 +63,7 @@ class ReportService:
else:
return UploadResult(
success=False,
message="Ошибка при сохранении в хранилище"
message="Ошибка при сохранении в хранилище. Возможно, MinIO недоступен."
)
finally:
@@ -86,7 +86,7 @@ class ReportService:
if not self.storage.object_exists(object_id):
return DataResult(
success=False,
message=f"Отчет типа '{request.report_type}' не найден"
message=f"Отчет типа '{request.report_type}' не найден. Возможно, MinIO недоступен или отчет не был загружен."
)
# Загружаем DataFrame из хранилища
@@ -94,7 +94,7 @@ class ReportService:
if df is None:
return DataResult(
success=False,
message="Ошибка при загрузке данных из хранилища"
message="Ошибка при загрузке данных из хранилища. Возможно, MinIO недоступен."
)
# Получаем парсер

View File

@@ -1,16 +1,32 @@
version: '3.8'
services:
minio:
image: minio/minio:latest
container_name: svodka_minio
ports:
- "9000:9000" # API порт
- "9001:9001" # Консоль порт
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000"
- "9001:9001"
volumes:
- ./minio:/data
command: server /data --console-address ":9001"
volumes:
- minio_data:/data
restart: unless-stopped
fastapi:
build: .
container_name: svodka_fastapi
ports:
- "8000:8000"
environment:
- MINIO_ENDPOINT=minio:9000
- MINIO_ACCESS_KEY=minioadmin
- MINIO_SECRET_KEY=minioadmin
- MINIO_SECURE=false
- MINIO_BUCKET=svodka-data
depends_on:
- minio
restart: unless-stopped
volumes:
minio_data:

View File

@@ -12,3 +12,4 @@ requests>=2.31.0
# pytest-mock>=3.10.0
httpx>=0.24.0
numpy
streamlit>=1.28.0

View File

@@ -4,7 +4,7 @@ if __name__ == "__main__":
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=8080,
port=8000, # Исправляем порт с 8080 на 8000
reload=True,
reload_dirs=["app", "core", "adapters"], # Папки для отслеживания изменений
reload_excludes=["*.pyc", "__pycache__", "*.log"], # Исключения

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""
Запуск Streamlit интерфейса для NIN Excel Parsers API
"""
import subprocess
import sys
import webbrowser
import time
def main():
"""Основная функция"""
print("🚀 ЗАПУСК STREAMLIT ИНТЕРФЕЙСА")
print("=" * 50)
print("Убедитесь, что FastAPI сервер запущен на порту 8000")
print("=" * 50)
# Проверяем, установлен ли Streamlit
try:
import streamlit
print(f"✅ Streamlit {streamlit.__version__} установлен")
except ImportError:
print("❌ Streamlit не установлен")
print("Установите: pip install streamlit")
return
print("\n🚀 Запускаю Streamlit...")
print("📍 URL: http://localhost:8501")
print("🛑 Для остановки нажмите Ctrl+C")
# Открываем браузер
try:
webbrowser.open("http://localhost:8501")
print("✅ Браузер открыт")
except Exception as e:
print(f"⚠️ Не удалось открыть браузер: {e}")
# Запускаем Streamlit
try:
subprocess.run([
sys.executable, "-m", "streamlit", "run", "streamlit_app.py",
"--server.port", "8501",
"--server.address", "localhost",
"--server.headless", "false",
"--browser.gatherUsageStats", "false"
])
except KeyboardInterrupt:
print("\n👋 Streamlit остановлен")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,402 @@
import streamlit as st
import requests
import json
import pandas as pd
import io
import zipfile
from typing import Dict, Any
import os
# Конфигурация страницы
st.set_page_config(
page_title="NIN Excel Parsers API Demo",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded"
)
# Конфигурация API
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8000")
def check_api_health():
"""Проверка доступности API"""
try:
response = requests.get(f"{API_BASE_URL}/", timeout=5)
return response.status_code == 200
except:
return False
def get_available_parsers():
"""Получение списка доступных парсеров"""
try:
response = requests.get(f"{API_BASE_URL}/parsers")
if response.status_code == 200:
return response.json()["parsers"]
return []
except:
return []
def get_server_info():
"""Получение информации о сервере"""
try:
response = requests.get(f"{API_BASE_URL}/server-info")
if response.status_code == 200:
return response.json()
return {}
except:
return {}
def upload_file_to_api(endpoint: str, file_data: bytes, filename: str):
"""Загрузка файла на API"""
try:
files = {"zip_file": (filename, file_data, "application/zip")}
response = requests.post(f"{API_BASE_URL}{endpoint}", files=files)
return response.json(), response.status_code
except Exception as e:
return {"error": str(e)}, 500
def make_api_request(endpoint: str, data: Dict[str, Any]):
"""Выполнение API запроса"""
try:
response = requests.post(f"{API_BASE_URL}{endpoint}", json=data)
return response.json(), response.status_code
except Exception as e:
return {"error": str(e)}, 500
def main():
st.title("🚀 NIN Excel Parsers API - Демонстрация")
st.markdown("---")
# Проверка доступности API
if not check_api_health():
st.error(f"❌ API недоступен по адресу {API_BASE_URL}")
st.info("Убедитесь, что FastAPI сервер запущен")
return
st.success(f"✅ API доступен по адресу {API_BASE_URL}")
# Боковая панель с информацией
with st.sidebar:
st.header(" Информация")
# Информация о сервере
server_info = get_server_info()
if server_info:
st.subheader("Сервер")
st.write(f"PID: {server_info.get('process_id', 'N/A')}")
st.write(f"CPU ядер: {server_info.get('cpu_cores', 'N/A')}")
st.write(f"Память: {server_info.get('memory_mb', 'N/A'):.1f} MB")
# Доступные парсеры
parsers = get_available_parsers()
if parsers:
st.subheader("Доступные парсеры")
for parser in parsers:
st.write(f"{parser}")
# Основные вкладки
tab1, tab2, tab3, tab4 = st.tabs([
"📤 Загрузка файлов",
"📊 Сводки ПМ",
"🏭 Сводки СА",
"⛽ Мониторинг топлива"
])
# Вкладка 1: Загрузка файлов
with tab1:
st.header("📤 Загрузка файлов")
col1, col2 = st.columns(2)
with col1:
st.subheader("Сводки ПМ")
uploaded_pm = st.file_uploader(
"Выберите ZIP архив со сводками ПМ",
type=['zip'],
key="pm_upload"
)
if uploaded_pm is not None:
if st.button("📤 Загрузить сводки ПМ", key="upload_pm_btn"):
with st.spinner("Загружаю файл..."):
result, status = upload_file_to_api(
"/svodka_pm/upload-zip",
uploaded_pm.read(),
uploaded_pm.name
)
if status == 200:
st.success(f"{result.get('message', 'Файл загружен')}")
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
with col2:
st.subheader("Мониторинг топлива")
uploaded_fuel = st.file_uploader(
"Выберите ZIP архив с мониторингом топлива",
type=['zip'],
key="fuel_upload"
)
if uploaded_fuel is not None:
if st.button("📤 Загрузить мониторинг топлива", key="upload_fuel_btn"):
with st.spinner("Загружаю файл..."):
result, status = upload_file_to_api(
"/monitoring_fuel/upload-zip",
uploaded_fuel.read(),
uploaded_fuel.name
)
if status == 200:
st.success(f"{result.get('message', 'Файл загружен')}")
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
# Загрузка сводки СА
st.subheader("Сводка СА")
uploaded_ca = st.file_uploader(
"Выберите Excel файл сводки СА",
type=['xlsx', 'xlsm', 'xls'],
key="ca_upload"
)
if uploaded_ca is not None:
if st.button("📤 Загрузить сводку СА", key="upload_ca_btn"):
with st.spinner("Загружаю файл..."):
try:
files = {"file": (uploaded_ca.name, uploaded_ca.read(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
response = requests.post(f"{API_BASE_URL}/svodka_ca/upload", files=files)
result = response.json()
if response.status_code == 200:
st.success(f"{result.get('message', 'Файл загружен')}")
st.info(f"ID объекта: {result.get('object_id', 'N/A')}")
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
except Exception as e:
st.error(f"❌ Ошибка: {str(e)}")
# Вкладка 2: Сводки ПМ
with tab2:
st.header("📊 Сводки ПМ")
col1, col2 = st.columns(2)
with col1:
st.subheader("Данные по одному ОГ")
og_id = st.selectbox(
"Выберите ОГ",
["SNPZ", "KNPZ", "ANHK", "AchNPZ", "UNPZ", "UNH", "NOV",
"NovKuybNPZ", "KuybNPZ", "CyzNPZ", "TuapsNPZ", "RNPK",
"NVNPO", "KLNPZ", "PurNP", "YANOS"],
key="pm_single_og"
)
codes = st.multiselect(
"Выберите коды строк",
[78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
default=[78, 79],
key="pm_single_codes"
)
columns = st.multiselect(
"Выберите столбцы",
["ПП", "БП", "ТБ", "СЭБ", "НЭБ"],
default=["ПП", "СЭБ"],
key="pm_single_columns"
)
search = st.selectbox(
"Фильтр",
[None, "Итого"],
key="pm_single_search"
)
if st.button("🔍 Получить данные", key="pm_single_btn"):
if codes and columns:
with st.spinner("Получаю данные..."):
data = {
"id": og_id,
"codes": codes,
"columns": columns
}
if search:
data["search"] = search
result, status = make_api_request("/svodka_pm/get_single_og", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите коды и столбцы")
with col2:
st.subheader("Данные по всем ОГ")
codes_total = st.multiselect(
"Выберите коды строк",
[78, 79, 394, 395, 396, 397, 81, 82, 83, 84],
default=[78, 79],
key="pm_total_codes"
)
columns_total = st.multiselect(
"Выберите столбцы",
["ПП", "БП", "ТБ", "СЭБ", "НЭБ"],
default=["ПП", "СЭБ"],
key="pm_total_columns"
)
search_total = st.selectbox(
"Фильтр",
[None, "Итого"],
key="pm_total_search"
)
if st.button("🔍 Получить данные по всем ОГ", key="pm_total_btn"):
if codes_total and columns_total:
with st.spinner("Получаю данные..."):
data = {
"codes": codes_total,
"columns": columns_total
}
if search_total:
data["search"] = search_total
result, status = make_api_request("/svodka_pm/get_total_ogs", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите коды и столбцы")
# Вкладка 3: Сводки СА
with tab3:
st.header("🏭 Сводки СА")
st.subheader("Получение данных из сводки СА")
modes = st.multiselect(
"Выберите режимы",
["plan", "fact", "normativ"],
default=["plan", "fact"],
key="ca_modes"
)
tables = st.multiselect(
"Выберите таблицы",
["ТиП, %", "Топливо итого, тонн", "Топливо итого, %",
"Топливо на технологию, тонн", "Топливо на технологию, %",
"Топливо на энергетику, тонн", "Топливо на энергетику, %",
"Потери итого, тонн", "Потери итого, %"],
default=["ТиП, %", "Топливо итого, тонн"],
key="ca_tables"
)
if st.button("🔍 Получить данные СА", key="ca_btn"):
if modes and tables:
with st.spinner("Получаю данные..."):
data = {
"modes": modes,
"tables": tables
}
result, status = make_api_request("/svodka_ca/get_data", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите режимы и таблицы")
# Вкладка 4: Мониторинг топлива
with tab4:
st.header("⛽ Мониторинг топлива")
col1, col2 = st.columns(2)
with col1:
st.subheader("Агрегация по колонкам")
columns_fuel = st.multiselect(
"Выберите столбцы",
["normativ", "total", "total_1"],
default=["normativ", "total"],
key="fuel_columns"
)
if st.button("🔍 Получить агрегированные данные", key="fuel_total_btn"):
if columns_fuel:
with st.spinner("Получаю данные..."):
data = {
"columns": columns_fuel
}
result, status = make_api_request("/monitoring_fuel/get_total_by_columns", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
else:
st.warning("⚠️ Выберите столбцы")
with col2:
st.subheader("Данные за месяц")
month = st.selectbox(
"Выберите месяц",
[f"{i:02d}" for i in range(1, 13)],
key="fuel_month"
)
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
with st.spinner("Получаю данные..."):
data = {
"month": month
}
result, status = make_api_request("/monitoring_fuel/get_month_by_code", data)
if status == 200:
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
# Футер
st.markdown("---")
st.markdown("### 📚 Документация API")
st.markdown(f"Полная документация доступна по адресу: {API_BASE_URL}/docs")
# Информация о проекте
with st.expander(" О проекте"):
st.markdown("""
**NIN Excel Parsers API** - это веб-сервис для парсинга и обработки Excel-файлов нефтеперерабатывающих заводов.
**Возможности:**
- 📊 Парсинг сводок ПМ (план и факт)
- 🏭 Парсинг сводок СА
- ⛽ Мониторинг топлива
**Технологии:**
- FastAPI
- Pandas
- MinIO (S3-совместимое хранилище)
- Streamlit (веб-интерфейс)
""")
if __name__ == "__main__":
main()