3 Commits

Author SHA1 Message Date
4cbdaf1b60 ch 2025-09-01 13:58:42 +03:00
9459196804 all in docker 2025-09-01 12:24:37 +03:00
ce228d9756 work 2025-09-01 12:08:16 +03:00
31 changed files with 1480 additions and 805 deletions

204
.gitignore vendored
View File

@@ -1,8 +1,15 @@
# Python
data
.streamlit
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
@@ -16,13 +23,88 @@ parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
# 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/
@@ -31,86 +113,6 @@ ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Desktop.ini
# Logs
*.log
logs/
log/
# MinIO data and cache
minio_data/
.minio.sys/
*.meta
part.*
# Docker
.dockerignore
docker-compose.override.yml
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Temporary files
*.tmp
*.temp
*.bak
*.backup
*.orig
# Data files (Excel, CSV, etc.)
*.xlsx
*.xls
*.xlsm
*.csv
*.json
data/
uploads/
# Cache directories
.cache/
.pytest_cache/
.coverage
htmlcov/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# pipenv
Pipfile.lock
# poetry
poetry.lock
# Celery
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Spyder project settings
.spyderproject
.spyproject
@@ -129,27 +131,23 @@ dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Cython debug symbols
cython_debug/
# OS
.DS_Store
Thumbs.db
# Local development
local_settings.py
db.sqlite3
db.sqlite3-journal
# Project specific
data/
*.zip
*.xlsx
*.xls
*.xlsm
# FastAPI
.pytest_cache/
.coverage
htmlcov/
# Streamlit
.streamlit/secrets.toml
# Node.js (if any frontend components)
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# MinIO data directory
minio_data/

1
Procfile Normal file
View File

@@ -0,0 +1 @@
web: python /app/run_stand.py

View File

@@ -1,41 +0,0 @@
# 🚀 Быстрый запуск проекта
## 1. Запуск всех сервисов
```bash
docker compose up -d
```
## 2. Проверка статуса
```bash
docker compose ps
```
## 3. Доступ к сервисам
- **FastAPI**: http://localhost:8000
- **Streamlit**: http://localhost:8501
- **MinIO Console**: http://localhost:9001
- **MinIO API**: http://localhost:9000
## 4. Остановка
```bash
docker compose down
```
## 5. Просмотр логов
```bash
# Все сервисы
docker compose logs
# Конкретный сервис
docker compose logs fastapi
docker compose logs streamlit
docker compose logs minio
```
## 6. Пересборка и перезапуск
```bash
docker compose up -d --build
```
---
**Примечание**: При первом запуске Docker будет скачивать образы и собирать контейнеры, это может занять несколько минут.

223
README.md
View File

@@ -1,117 +1,182 @@
# Python Parser CF - Система анализа данных
# 🚀 NIN Excel Parsers API - Полная система
Проект состоит из трех основных компонентов:
- **python_parser** - FastAPI приложение для парсинга и обработки данных
- **streamlit_app** - Streamlit приложение для визуализации и анализа
- **minio_data** - хранилище данных MinIO
Полноценная система для парсинга Excel отчетов нефтеперерабатывающих заводов (НПЗ) с использованием FastAPI, MinIO и Streamlit.
## 🏗️ Архитектура проекта
Проект состоит из **двух изолированных пакетов**:
- **`python_parser/`** - FastAPI сервер + парсеры Excel
- **`streamlit_app/`** - Веб-интерфейс для демонстрации API
## 🚀 Быстрый запуск
### Предварительные требования
- Docker и Docker Compose
- Git
### Запуск всех сервисов (продакшн)
### **Вариант 1: Все сервисы в Docker (рекомендуется)**
```bash
docker compose up -d
# Запуск всех сервисов: 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
# Автоматический запуск
python start_dev.py
# Запуск MinIO в Docker
docker-compose up -d minio
# Или вручную
docker compose -f docker-compose.dev.yml up -d
# Запуск FastAPI локально
cd python_parser
python run_dev.py
# В отдельном терминале - Streamlit
cd streamlit_app
streamlit run app.py
```
**Режим разработки** позволяет:
- Автоматически перезагружать Streamlit при изменении кода
- Монтировать исходный код напрямую в контейнер
- Видеть изменения без пересборки контейнеров
### Доступ к сервисам
- **FastAPI**: http://localhost:8000
- **Streamlit**: http://localhost:8501
- **MinIO Console**: http://localhost:9001
- **MinIO API**: http://localhost:9000
### Остановка сервисов
### **Вариант 3: Только MinIO в Docker**
```bash
docker-compose down
# Запуск только MinIO
docker-compose up -d minio
```
## 📋 Описание сервисов
- **MinIO** (порт 9000-9001): S3-совместимое хранилище для данных
- **FastAPI** (порт 8000): API сервер для парсинга Excel файлов
- **Streamlit** (порт 8501): Веб-интерфейс для демонстрации API
## 📁 Структура проекта
```
python_parser_cf/
├── python_parser/ # FastAPI приложение
│ ├── app/ # Основной код приложения
│ ├── adapters/ # Адаптеры для парсеров
├── core/ # Основная бизнес-логика
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
├── streamlit_app/ # Streamlit приложение
── streamlit_app.py # Основной файл приложения
│ ├── requirements.txt # Зависимости Python
── Dockerfile # Docker образ для FastAPI
│ ├── requirements.txt # Зависимости FastAPI
── run_dev.py # Запуск FastAPI локально
├── streamlit_app/ # Пакет Streamlit
│ ├── app.py # Основное Streamlit приложение
│ ├── requirements.txt # Зависимости Streamlit
│ ├── Dockerfile # Docker образ для Streamlit
│ ├── .streamlit/ # Конфигурация Streamlit
│ └── Dockerfile # Docker образ для Streamlit
├── minio_data/ # Данные для MinIO
├── docker-compose.yml # Конфигурация всех сервисов
── README.md # Документация
│ └── config.toml # Настройки
│ └── README.md # Документация Streamlit
├── docker-compose.yml # Docker Compose конфигурация
── .gitignore # Git исключения
└── README.md # Общая документация
```
## 🔧 Конфигурация
## 🔍 Доступные эндпоинты
### Переменные окружения
Все сервисы используют следующие переменные окружения:
- `MINIO_ENDPOINT` - адрес MinIO сервера
- `MINIO_ACCESS_KEY` - ключ доступа к MinIO
- `MINIO_SECRET_KEY` - секретный ключ MinIO
- `MINIO_SECURE` - использование SSL/TLS
- `MINIO_BUCKET` - имя bucket'а для данных
- **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** - Получение данных мониторинга топлива
### Порты
- **8000** - FastAPI
- **8501** - Streamlit
- **9000** - MinIO API
- **9001** - MinIO Console
## 📊 Поддерживаемые типы отчетов
## 📊 Использование
1. **svodka_pm** - Сводки по переработке нефти (ПМ)
- Геттеры: `single_og`, `total_ogs`
2. **svodka_ca** - Сводки по переработке нефти (ЦА)
- Геттеры: `get_data`
3. **monitoring_fuel** - Мониторинг топлива
- Геттеры: `total_by_columns`, `month_by_code`
1. **Запустите все сервисы**: `docker-compose up -d`
2. **Откройте Streamlit**: http://localhost:8501
3. **Выберите тип данных** для анализа
4. **Просматривайте результаты** в интерактивном интерфейсе
## 🏗️ Архитектура
## 🛠️ Разработка
Проект использует **Hexagonal Architecture (Ports and Adapters)**:
### Режим разработки (рекомендуется)
- **Порты (Ports)**: Интерфейсы для бизнес-логики
- **Адаптеры (Adapters)**: Реализации для внешних систем
- **Сервисы (Services)**: Бизнес-логика приложения
### Система геттеров парсеров
Каждый парсер может иметь несколько методов получения данных (геттеров):
- Регистрация геттеров в словаре с метаданными
- Валидация параметров для каждого геттера
- Единый интерфейс `get_value(getter_name, params)`
## 🐳 Docker
### Сборка образов:
```bash
# Запуск режима разработки
python start_dev.py
# FastAPI
docker build -t nin-fastapi ./python_parser
# Остановка
docker compose -f docker-compose.dev.yml down
# Возврат к продакшн режиму
python start_prod.py
# Streamlit
docker build -t nin-streamlit ./streamlit_app
```
### Локальная разработка FastAPI
### Запуск отдельных сервисов:
```bash
# Только MinIO
docker-compose up -d minio
# MinIO + FastAPI
docker-compose up -d minio fastapi
# Все сервисы
docker-compose up -d
```
## 🛑 Остановка
### Остановка Docker сервисов:
```bash
# Все сервисы
docker-compose down
# Только MinIO
docker-compose stop minio
```
### Остановка локальных сервисов:
```bash
# Нажмите Ctrl+C в терминале с FastAPI/Streamlit
```
## 🔧 Разработка
### Добавление нового парсера:
1. Создайте файл в `python_parser/adapters/parsers/`
2. Реализуйте интерфейс `ParserPort`
3. Добавьте в `python_parser/core/services.py`
4. Создайте схемы в `python_parser/app/schemas/`
5. Добавьте эндпоинты в `python_parser/app/main.py`
### Тестирование:
```bash
# Запуск тестов
cd python_parser
pip install -r requirements.txt
uvicorn app.main:app --reload
```
pytest
### Локальная разработка Streamlit
```bash
cd streamlit_app
pip install -r requirements.txt
streamlit run streamlit_app.py
# Запуск с покрытием
pytest --cov=.
```
## 📝 Лицензия
Проект разработан для внутреннего использования.
Проект разработан для внутреннего использования НИН.

View File

@@ -170,16 +170,11 @@ def main():
if not port_8000_ok:
print("\n🔧 РЕШЕНИЕ: Запустите FastAPI сервер")
print("python run_dev.py")
print("docker-compose up -d fastapi")
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")
print("docker-compose up -d streamlit")
if __name__ == "__main__":
main()

34
create_test_excel.py Normal file
View 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()

View File

@@ -1,58 +0,0 @@
services:
minio:
image: minio/minio:latest
container_name: svodka_minio_dev
ports:
- "9000:9000" # API порт
- "9001:9001" # Консоль порт
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
command: server /data --console-address ":9001"
volumes:
- ./minio_data:/data
restart: unless-stopped
fastapi:
build: ./python_parser
container_name: svodka_fastapi_dev
ports:
- "8000:8000"
environment:
- MINIO_ENDPOINT=minio:9000
- MINIO_ACCESS_KEY=minioadmin
- MINIO_SECRET_KEY=minioadmin
- MINIO_SECURE=false
- MINIO_BUCKET=svodka-data
depends_on:
- minio
restart: unless-stopped
streamlit:
image: python:3.11-slim
container_name: svodka_streamlit_dev
ports:
- "8501:8501"
environment:
- API_BASE_URL=http://fastapi:8000
- API_PUBLIC_URL=http://localhost:8000
- MINIO_ENDPOINT=minio:9000
- MINIO_ACCESS_KEY=minioadmin
- MINIO_SECRET_KEY=minioadmin
- MINIO_SECURE=false
- MINIO_BUCKET=svodka-data
volumes:
# Монтируем исходный код для автоматической перезагрузки
- ./streamlit_app:/app
# Монтируем requirements.txt для установки зависимостей
- ./streamlit_app/requirements.txt:/app/requirements.txt
working_dir: /app
depends_on:
- minio
- fastapi
restart: unless-stopped
command: >
bash -c "
pip install --no-cache-dir -r requirements.txt &&
streamlit run streamlit_app.py --server.port=8501 --server.address=0.0.0.0 --server.runOnSave=true
"

View File

@@ -1,5 +1,3 @@
# Продакшн конфигурация
# Для разработки используйте: docker compose -f docker-compose.dev.yml up -d
services:
minio:
image: minio/minio:latest
@@ -37,13 +35,7 @@ services:
- "8501:8501"
environment:
- API_BASE_URL=http://fastapi:8000
- API_PUBLIC_URL=http://localhost:8000
- MINIO_ENDPOINT=minio:9000
- MINIO_ACCESS_KEY=minioadmin
- MINIO_SECRET_KEY=minioadmin
- MINIO_SECURE=false
- MINIO_BUCKET=svodka-data
- DOCKER_ENV=true
depends_on:
- minio
- fastapi
restart: unless-stopped

17
manifest.yml Normal file
View File

@@ -0,0 +1,17 @@
applications:
- name: nin-python-parser-dev-test
buildpack: python_buildpack
health-check-type: web
services:
- logging-shared-dev
command: python /app/run_stand.py
path: .
disk_quota: 2G
memory: 4G
instances: 1
env:
MINIO_ENDPOINT: s3-region1.ppc-jv-dev.sibintek.ru
MINIO_ACCESS_KEY: 00a70fac02c1208446de
MINIO_SECRET_KEY: 1gk9tVYEEoH9ADRxb4kiAuCo6CCISdV6ie0p6oDO
MINIO_BUCKET: bucket-476684e7-1223-45ac-a101-8b5aeda487d6
MINIO_SECURE: false

20
python_parser/Dockerfile_ Normal file
View 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"]

104
python_parser/README.md Normal file
View File

@@ -0,0 +1,104 @@
# 📊 Python Parser - FastAPI + Парсеры Excel
Пакет FastAPI сервера и парсеров Excel для нефтеперерабатывающих заводов.
## 🚀 Быстрый запуск
### **Локально:**
```bash
# Установка зависимостей
pip install -r requirements.txt
# Запуск FastAPI сервера
python run_dev.py
```
### **В Docker:**
```bash
# Сборка образа
docker build -t nin-fastapi .
# Запуск контейнера
docker run -p 8000:8000 nin-fastapi
```
## 📁 Структура пакета
```
python_parser/
├── app/ # FastAPI приложение
│ ├── main.py # Основной файл приложения
│ └── schemas/ # Pydantic схемы
├── core/ # Бизнес-логика
│ ├── models.py # Модели данных
│ ├── ports.py # Интерфейсы (порты)
│ └── services.py # Сервисы
├── adapters/ # Адаптеры для внешних систем
│ ├── storage.py # MinIO адаптер
│ └── parsers/ # Парсеры Excel файлов
├── data/ # Тестовые данные
├── Dockerfile # Docker образ для FastAPI
├── requirements.txt # Зависимости Python
└── run_dev.py # Запуск FastAPI локально
```
## 🔍 Основные эндпоинты
- **GET /** - Информация об API
- **GET /docs** - Swagger документация
- **GET /parsers** - Список доступных парсеров
- **GET /parsers/{parser_name}/getters** - Информация о геттерах парсера
- **POST /svodka_pm/upload-zip** - Загрузка сводок ПМ
- **POST /svodka_ca/upload** - Загрузка сводок ЦА
- **POST /monitoring_fuel/upload-zip** - Загрузка мониторинга топлива
- **POST /svodka_pm/get_data** - Получение данных сводок ПМ
- **POST /svodka_ca/get_data** - Получение данных сводок ЦА
- **POST /monitoring_fuel/get_data** - Получение данных мониторинга топлива
## 📊 Поддерживаемые парсеры
1. **svodka_pm** - Сводки по переработке нефти (ПМ)
- Геттеры: `single_og`, `total_ogs`
2. **svodka_ca** - Сводки по переработке нефти (ЦА)
- Геттеры: `get_data`
3. **monitoring_fuel** - Мониторинг топлива
- Геттеры: `total_by_columns`, `month_by_code`
## 🏗️ Архитектура
Использует **Hexagonal Architecture (Ports and Adapters)**:
- **Порты (Ports)**: Интерфейсы для бизнес-логики
- **Адаптеры (Adapters)**: Реализации для внешних систем
- **Сервисы (Services)**: Бизнес-логика приложения
### Система геттеров парсеров
Каждый парсер может иметь несколько методов получения данных (геттеров):
- Регистрация геттеров в словаре с метаданными
- Валидация параметров для каждого геттера
- Единый интерфейс `get_value(getter_name, params)`
## 🔧 Разработка
### Добавление нового парсера:
1. Создайте файл в `adapters/parsers/`
2. Реализуйте интерфейс `ParserPort`
3. Добавьте в `core/services.py`
4. Создайте схемы в `app/schemas/`
5. Добавьте эндпоинты в `app/main.py`
### Тестирование:
```bash
# Запуск тестов
pytest
# Запуск с покрытием
pytest --cov=.
```
## 📝 Примечание
Этот пакет является частью большей системы. Для полной документации и запуска всех сервисов см. README.md в корне проекта.

View File

@@ -1,9 +1,9 @@
import pandas as pd
import re
from typing import Dict
import zipfile
from typing import Dict, Tuple
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):
@@ -11,71 +11,55 @@ class MonitoringFuelParser(ParserPort):
name = "Мониторинг топлива"
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
def _register_default_getters(self):
"""Регистрация геттеров по умолчанию"""
self.register_getter(
name="total_by_columns",
method=self._get_total_by_columns,
required_params=["columns"],
optional_params=[],
description="Агрегация данных по колонкам"
)
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
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
self.register_getter(
name="month_by_code",
method=self._get_month_by_code,
required_params=["month"],
optional_params=[],
description="Получение данных за конкретный месяц"
)
# === Удаление полностью пустых столбцов ===
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] # оставляем только непустые столбцы
def _get_total_by_columns(self, params: dict):
"""Агрегация по колонкам (обертка для совместимости)"""
columns = params["columns"]
if not columns:
raise ValueError("Отсутствуют идентификаторы столбцов")
# === Переименовываем нужные столбцы по позициям ===
if len(df_full.columns) < 2:
raise ValueError("DataFrame должен содержать как минимум 2 столбца.")
# TODO: Переделать под новую архитектуру
df_means, _ = self.aggregate_by_columns(self.df, columns)
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'
new_columns[1] = 'normativ'
new_columns[-2] = 'total'
new_columns[-1] = 'total_1'
# TODO: Переделать под новую архитектуру
df_month = self.get_month(self.df, month)
return df_month.to_dict(orient='index')
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' существует
if 'name' in df_full.columns:
# Применяем функцию get_id_by_name к каждой строке в колонке 'name'
df_full['id'] = df_full['name'].apply(get_object_by_name)
# Устанавливаем id как индекс
df_full.set_index('id', inplace=True)
print(f"Окончательное количество столбцов: {len(df_full.columns)}")
return df_full
def parse(self, file_path: str, params: dict) -> dict:
import zipfile
def parse_monitoring_fuel_files(self, zip_path: str, params: dict) -> Dict[str, pd.DataFrame]:
"""Парсинг ZIP архива с файлами мониторинга топлива"""
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()
for month in range(1, 13):
@@ -103,7 +87,70 @@ class MonitoringFuelParser(ParserPort):
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 = {} # Для хранения полных данных (месяцы) по каждой колонке
means = {} # Для хранения средних
@@ -185,22 +232,3 @@ class MonitoringFuelParser(ParserPort):
total.name = 'mean'
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

View File

@@ -6,85 +6,48 @@ from adapters.pconfig import get_og_by_name
class SvodkaCAParser(ParserPort):
"""Парсер для сводки СА"""
"""Парсер для сводок СА"""
name = "Сводка СА"
name = "Сводки СА"
def extract_all_tables(self, file_path, sheet_name=0):
"""Извлекает все таблицы из Excel файла"""
df = pd.read_excel(file_path, sheet_name=sheet_name, header=None)
df_filled = df.fillna('')
df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True)
def _register_default_getters(self):
"""Регистрация геттеров по умолчанию"""
self.register_getter(
name="get_data",
method=self._get_data_wrapper,
required_params=["modes", "tables"],
optional_params=[],
description="Получение данных по режимам и таблицам"
)
non_empty_rows = ~(df_clean.eq('').all(axis=1))
non_empty_cols = ~(df_clean.eq('').all(axis=0))
def _get_data_wrapper(self, params: dict):
"""Обертка для получения данных (для совместимости)"""
modes = params["modes"]
tables = params["tables"]
row_indices = non_empty_rows[non_empty_rows].index.tolist()
col_indices = non_empty_cols[non_empty_cols].index.tolist()
if not isinstance(modes, list):
raise ValueError("Поле 'modes' должно быть списком")
if not isinstance(tables, list):
raise ValueError("Поле 'tables' должно быть списком")
if not row_indices or not col_indices:
return []
# TODO: Переделать под новую архитектуру
data_dict = {}
for mode in modes:
data_dict[mode] = self.get_data(self.df, mode, tables)
return self.data_dict_to_json(data_dict)
row_blocks = self._get_contiguous_blocks(row_indices)
col_blocks = self._get_contiguous_blocks(col_indices)
def parse(self, file_path: str, params: dict) -> pd.DataFrame:
"""Парсинг файла и возврат DataFrame"""
# Сохраняем DataFrame для использования в геттерах
self.df = self.parse_svodka_ca(file_path, params)
return self.df
tables = []
for r_start, r_end in row_blocks:
for c_start, c_end in col_blocks:
block = df.iloc[r_start:r_end + 1, c_start:c_end + 1]
if block.empty or block.fillna('').astype(str).replace(r'^\s*$', '', regex=True).eq('').all().all():
continue
def parse_svodka_ca(self, file_path: str, params: dict) -> dict:
"""Парсинг сводки СА"""
# Получаем параметры из params
sheet_name = params.get('sheet_name', 0) # По умолчанию первый лист
inclusion_list = params.get('inclusion_list', {'ТиП', 'Топливо', 'Потери'})
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)
@@ -190,76 +153,185 @@ class SvodkaCAParser(ParserPort):
else:
return None
def parse(self, file_path: str, params: dict) -> dict:
"""Парсинг файла сводки СА"""
# === Точка входа. Нужно выгрузить три таблицы: План, Факт и Норматив ===
# Выгружаем План в df_ca_plan
inclusion_list_plan = {
"ТиП, %",
"Топливо итого, тонн",
"Топливо итого, %",
"Топливо на технологию, тонн",
"Топливо на технологию, %",
"Топливо на энергетику, тонн",
"Топливо на энергетику, %",
"Потери итого, тонн",
"Потери итого, %",
"в т.ч. Идентифицированные безвозвратные потери, тонн**",
"в т.ч. Идентифицированные безвозвратные потери, %**",
"в т.ч. Неидентифицированные потери, тонн**",
"в т.ч. Неидентифицированные потери, %**"
}
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_filled = df.fillna('')
df_clean = df_filled.astype(str).replace(r'^\s*$', '', regex=True)
df_ca_plan = self.parse_sheet(file_path, 'План', inclusion_list_plan) # ЭТО ДАТАФРЕЙМ ПЛАНА В СВОДКЕ ЦА
print(f"\n--- Объединённый и отсортированный План: {df_ca_plan.shape} ---")
non_empty_rows = ~(df_clean.eq('').all(axis=1))
non_empty_cols = ~(df_clean.eq('').all(axis=0))
# Выгружаем Факт
inclusion_list_fact = {
"ТиП, %",
"Топливо итого, тонн",
"Топливо итого, %",
"Топливо на технологию, тонн",
"Топливо на технологию, %",
"Топливо на энергетику, тонн",
"Топливо на энергетику, %",
"Потери итого, тонн",
"Потери итого, %",
"в т.ч. Идентифицированные безвозвратные потери, тонн",
"в т.ч. Идентифицированные безвозвратные потери, %",
"в т.ч. Неидентифицированные потери, тонн",
"в т.ч. Неидентифицированные потери, %"
}
row_indices = non_empty_rows[non_empty_rows].index.tolist()
col_indices = non_empty_cols[non_empty_cols].index.tolist()
df_ca_fact = self.parse_sheet(file_path, 'Факт', inclusion_list_fact) # ЭТО ДАТАФРЕЙМ ФАКТА В СВОДКЕ ЦА
print(f"\n--- Объединённый и отсортированный Факт: {df_ca_fact.shape} ---")
if not row_indices or not col_indices:
return []
# Выгружаем План в df_ca_normativ
inclusion_list_normativ = {
"Топливо итого, тонн",
"Топливо итого, %",
"Топливо на технологию, тонн",
"Топливо на технологию, %",
"Топливо на энергетику, тонн",
"Топливо на энергетику, %",
"Потери итого, тонн",
"Потери итого, %",
"в т.ч. Идентифицированные безвозвратные потери, тонн**",
"в т.ч. Идентифицированные безвозвратные потери, %**",
"в т.ч. Неидентифицированные потери, тонн**",
"в т.ч. Неидентифицированные потери, %**"
}
row_blocks = self._get_contiguous_blocks(row_indices)
col_blocks = self._get_contiguous_blocks(col_indices)
# ЭТО ДАТАФРЕЙМ НОРМАТИВА В СВОДКЕ ЦА
df_ca_normativ = self.parse_sheet(file_path, 'Норматив', inclusion_list_normativ)
tables = []
for r_start, r_end in row_blocks:
for c_start, c_end in col_blocks:
block = df.iloc[r_start:r_end + 1, c_start:c_end + 1]
if block.empty or block.fillna('').astype(str).replace(r'^\s*$', '', regex=True).eq('').all().all():
continue
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 = {
"plan": df_ca_plan,
"fact": df_ca_fact,
"normativ": df_ca_normativ
}
return df_dict
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: 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):
''' Служебная функция для парсинга словаря в json. '''
@@ -308,17 +380,3 @@ class SvodkaCAParser(ParserPort):
filtered_df = df[df['table'].isin(table_values)].copy()
result_dict = {key: group for key, group in filtered_df.groupby('table')}
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)

View File

@@ -9,6 +9,60 @@ class SvodkaPMParser(ParserPort):
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:
"""Определения индекса заголовка в excel по ключевому слову"""
# Читаем первые max_rows строк без заголовков
@@ -16,7 +70,8 @@ class SvodkaPMParser(ParserPort):
file,
sheet_name=sheet,
header=None,
nrows=max_rows
nrows=max_rows,
engine='openpyxl'
)
# Ищем строку, где хотя бы в одном столбце встречается искомое значение
@@ -40,6 +95,7 @@ class SvodkaPMParser(ParserPort):
header=header_num,
usecols=None,
nrows=2,
engine='openpyxl'
)
if df_probe.shape[0] == 0:
@@ -61,7 +117,8 @@ class SvodkaPMParser(ParserPort):
sheet_name=sheet,
header=header_num,
usecols=None,
index_col=None
index_col=None,
engine='openpyxl'
)
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'
# Проверяем, начинается ли на "Итого"
if col_str.startswith('Итого'):
current_name = 'Итого'
last_good_name = current_name # обновляем last_good_name
new_columns.append(current_name)
elif is_empty_or_unnamed:
# Используем последнее хорошее имя
if is_empty_or_unnamed:
# Если это пустая колонка, используем последнее хорошее имя
if last_good_name:
new_columns.append(last_good_name)
else:
# Имя, полученное из exel
# Если нет хорошего имени, пропускаем
continue
else:
# Это хорошая колонка
last_good_name = col_str
new_columns.append(col_str)
# Применяем новые заголовки
df_final.columns = new_columns
print(f"Окончательное количество столбцов: {len(df_final.columns)}")
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
pm_dict = {
"facts": {},
@@ -125,7 +182,7 @@ class SvodkaPMParser(ParserPort):
}
excel_fact_template = 'svodka_fact_pm_ID.xlsm'
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()
for name, id in OG_IDS.items():
if id == 'BASH':
@@ -155,9 +212,9 @@ class SvodkaPMParser(ParserPort):
return pm_dict
def get_svodka_value(self, df_svodka, id, code, search_value=None):
''' Служебная функция для простой выборке по сводке '''
row_index = id
def get_svodka_value(self, df_svodka, code, search_value, search_value_filter=None):
''' Служебная функция получения значения по коду и столбцу '''
row_index = code
mask_value = df_svodka.iloc[0] == code
if search_value is None:
@@ -254,22 +311,4 @@ class SvodkaPMParser(ParserPort):
return total_result
def get_value(self, df, params):
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
# Убираем старый метод get_value, так как он теперь в базовом классе

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

@@ -0,0 +1 @@
python-3.11.*

65
run_streamlit_local.py Normal file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""
Запуск Streamlit интерфейса локально из изолированного пакета
"""
import subprocess
import sys
import webbrowser
import os
def main():
"""Основная функция"""
print("🚀 ЗАПУСК STREAMLIT ИЗ ИЗОЛИРОВАННОГО ПАКЕТА")
print("=" * 60)
print("Убедитесь, что FastAPI сервер запущен на порту 8000")
print("=" * 60)
# Проверяем, существует ли папка streamlit_app
if not os.path.exists("streamlit_app"):
print("❌ Папка streamlit_app не найдена")
print("Создайте изолированный пакет или используйте docker-compose up -d")
return
# Переходим в папку streamlit_app
os.chdir("streamlit_app")
# Проверяем, установлен ли Streamlit
try:
import streamlit
print(f"✅ Streamlit {streamlit.__version__} установлен")
except ImportError:
print("❌ Streamlit не установлен")
print("Установите: pip install -r requirements.txt")
return
print("\n🚀 Запускаю Streamlit...")
print("📍 URL: http://localhost:8501")
print("🔗 API: http://localhost:8000")
print("🛑 Для остановки нажмите Ctrl+C")
# Открываем браузер
try:
webbrowser.open("http://localhost:8501")
print("✅ Браузер открыт")
except Exception as e:
print(f"⚠️ Не удалось открыть браузер: {e}")
# Запускаем Streamlit с правильными переменными окружения
env = os.environ.copy()
env["DOCKER_ENV"] = "false" # Локальный запуск
env["API_BASE_URL"] = "http://localhost:8000" # Локальный API
try:
subprocess.run([
sys.executable, "-m", "streamlit", "run", "app.py",
"--server.port", "8501",
"--server.address", "localhost",
"--server.headless", "false",
"--browser.gatherUsageStats", "false"
], env=env)
except KeyboardInterrupt:
print("\n👋 Streamlit остановлен")
if __name__ == "__main__":
main()

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env python3
"""
Скрипт для запуска проекта в режиме разработки
"""
import subprocess
import sys
import os
def run_command(command, description):
"""Выполнение команды с выводом"""
print(f"🔄 {description}...")
try:
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
print(f"{description} выполнено успешно")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Ошибка при {description.lower()}:")
print(f" Команда: {command}")
print(f" Ошибка: {e.stderr}")
return False
def main():
print("🚀 Запуск проекта в режиме разработки")
print("=" * 50)
# Останавливаем продакшн контейнеры если они запущены
if run_command("docker compose ps", "Проверка статуса контейнеров"):
if "Up" in subprocess.run("docker compose ps", shell=True, capture_output=True, text=True).stdout:
print("🛑 Останавливаю продакшн контейнеры...")
run_command("docker compose down", "Остановка продакшн контейнеров")
# Запускаем режим разработки
print("\n🔧 Запуск режима разработки...")
if run_command("docker compose -f docker-compose.dev.yml up -d", "Запуск контейнеров разработки"):
print("\n🎉 Проект запущен в режиме разработки!")
print("\n📍 Доступные сервисы:")
print(" • Streamlit: http://localhost:8501")
print(" • FastAPI: http://localhost:8000")
print(" • MinIO Console: http://localhost:9001")
print("\n💡 Теперь изменения в streamlit_app/ будут автоматически перезагружаться!")
print("\n🛑 Для остановки используйте:")
print(" docker compose -f docker-compose.dev.yml down")
else:
print("\nНе удалось запустить проект в режиме разработки")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env python3
"""
Скрипт для запуска проекта в продакшн режиме
"""
import subprocess
import sys
def run_command(command, description):
"""Выполнение команды с выводом"""
print(f"🔄 {description}...")
try:
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
print(f"{description} выполнено успешно")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Ошибка при {description.lower()}:")
print(f" Команда: {command}")
print(f" Ошибка: {e.stderr}")
return False
def main():
print("🚀 Запуск проекта в продакшн режиме")
print("=" * 50)
# Останавливаем контейнеры разработки если они запущены
if run_command("docker compose -f docker-compose.dev.yml ps", "Проверка статуса контейнеров разработки"):
if "Up" in subprocess.run("docker compose -f docker-compose.dev.yml ps", shell=True, capture_output=True, text=True).stdout:
print("🛑 Останавливаю контейнеры разработки...")
run_command("docker compose -f docker-compose.dev.yml down", "Остановка контейнеров разработки")
# Запускаем продакшн режим
print("\n🏭 Запуск продакшн режима...")
if run_command("docker compose up -d --build", "Запуск продакшн контейнеров"):
print("\n🎉 Проект запущен в продакшн режиме!")
print("\n📍 Доступные сервисы:")
print(" • Streamlit: http://localhost:8501")
print(" • FastAPI: http://localhost:8000")
print(" • MinIO Console: http://localhost:9001")
print("\n💡 Для разработки используйте:")
print(" python start_dev.py")
print("\n🛑 Для остановки используйте:")
print(" docker compose down")
else:
print("\nНе удалось запустить проект в продакшн режиме")
sys.exit(1)
if __name__ == "__main__":
main()

View 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
*~

View File

@@ -1,15 +0,0 @@
[server]
port = 8501
address = "0.0.0.0"
enableCORS = false
enableXsrfProtection = false
[browser]
gatherUsageStats = false
[theme]
primaryColor = "#FF4B4B"
backgroundColor = "#FFFFFF"
secondaryBackgroundColor = "#F0F2F6"
textColor = "#262730"
font = "sans serif"

View File

@@ -2,22 +2,22 @@ FROM python:3.11-slim
WORKDIR /app
# Установка системных зависимостей
# Устанавливаем системные зависимости
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Копирование requirements.txt
# Копируем файлы зависимостей
COPY requirements.txt .
# Установка Python зависимостей
# Устанавливаем Python зависимости
RUN pip install --no-cache-dir -r requirements.txt
# Копирование кода приложения
# Копируем код приложения
COPY . .
# Открытие порта
# Открываем порт
EXPOSE 8501
# Запуск Streamlit
CMD ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
# Команда запуска
CMD ["streamlit", "run", "app.py", "--server.port", "8501", "--server.address", "0.0.0.0"]

44
streamlit_app/README.md Normal file
View 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**

View File

@@ -1,100 +0,0 @@
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from minio import Minio
import os
from io import BytesIO
# Конфигурация страницы
st.set_page_config(
page_title="Сводка данных",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded"
)
# Заголовок приложения
st.title("📊 Анализ данных сводки")
st.markdown("---")
# Инициализация MinIO клиента
@st.cache_resource
def init_minio_client():
try:
client = Minio(
os.getenv("MINIO_ENDPOINT", "localhost:9000"),
access_key=os.getenv("MINIO_ACCESS_KEY", "minioadmin"),
secret_key=os.getenv("MINIO_SECRET_KEY", "minioadmin"),
secure=os.getenv("MINIO_SECURE", "false").lower() == "true"
)
return client
except Exception as e:
st.error(f"Ошибка подключения к MinIO: {e}")
return None
# Боковая панель
with st.sidebar:
st.header("⚙️ Настройки")
# Выбор типа данных
data_type = st.selectbox(
"Тип данных",
["Мониторинг топлива", "Сводка ПМ", "Сводка ЦА"]
)
# Выбор периода
period = st.date_input(
"Период",
value=pd.Timestamp.now().date()
)
st.markdown("---")
st.markdown("### 📈 Статистика")
st.info("Выберите тип данных для анализа")
# Основной контент
col1, col2 = st.columns([2, 1])
with col1:
st.subheader(f"📋 {data_type}")
if data_type == "Мониторинг топлива":
st.info("Анализ данных мониторинга топлива")
# Здесь будет логика для работы с данными мониторинга топлива
elif data_type == "Сводка ПМ":
st.info("Анализ данных сводки ПМ")
# Здесь будет логика для работы с данными сводки ПМ
elif data_type == "Сводка ЦА":
st.info("Анализ данных сводки ЦА")
# Здесь будет логика для работы с данными сводки ЦА
with col2:
st.subheader("📊 Быстрая статистика")
st.metric("Всего записей", "0")
st.metric("Активных", "0")
st.metric("Ошибок", "0")
# Нижняя панель
st.markdown("---")
st.subheader("🔍 Детальный анализ")
# Заглушка для графиков
placeholder = st.empty()
with placeholder.container():
col1, col2 = st.columns(2)
with col1:
st.write("📈 График 1")
# Здесь будет график
with col2:
st.write("📊 График 2")
# Здесь будет график
# Футер
st.markdown("---")
st.markdown("**Разработано для анализа данных сводки** | v1.0.0")

View File

@@ -15,9 +15,17 @@ st.set_page_config(
initial_sidebar_state="expanded"
)
# Конфигурация API
API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker
API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя
# Конфигурация API - автоматически определяем правильный адрес
def get_api_base_url():
"""Автоматически определяет правильный адрес API"""
# Если запущено в Docker, используем внутренний адрес
if os.getenv("DOCKER_ENV") == "true":
return "http://fastapi:8000"
# Если запущено локально, используем localhost
return "http://localhost:8000"
API_BASE_URL = os.getenv("API_BASE_URL", get_api_base_url())
def check_api_health():
"""Проверка доступности API"""
@@ -37,6 +45,16 @@ def get_available_parsers():
except:
return []
def get_parser_getters(parser_name: str):
"""Получение информации о геттерах парсера"""
try:
response = requests.get(f"{API_BASE_URL}/parsers/{parser_name}/getters")
if response.status_code == 200:
return response.json()
return {}
except:
return {}
def get_server_info():
"""Получение информации о сервере"""
try:
@@ -74,7 +92,7 @@ def main():
st.info("Убедитесь, что FastAPI сервер запущен")
return
st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}")
st.success(f"✅ API доступен по адресу {API_BASE_URL}")
# Боковая панель с информацией
with st.sidebar:
@@ -106,6 +124,9 @@ def main():
with tab1:
st.header("📊 Сводки ПМ - Полный функционал")
# Получаем информацию о геттерах
getters_info = get_parser_getters("svodka_pm")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_pm = st.file_uploader(
@@ -134,6 +155,15 @@ def main():
# Секция получения данных
st.subheader("🔍 Получение данных")
# Показываем доступные геттеры
if getters_info and "getters" in getters_info:
st.info("📋 Доступные геттеры:")
for getter_name, getter_info in getters_info["getters"].items():
st.write(f"• **{getter_name}**: {getter_info.get('description', 'Нет описания')}")
st.write(f" - Обязательные параметры: {', '.join(getter_info.get('required_params', []))}")
if getter_info.get('optional_params'):
st.write(f" - Необязательные параметры: {', '.join(getter_info['optional_params'])}")
col1, col2 = st.columns(2)
with col1:
@@ -165,12 +195,13 @@ def main():
if codes and columns:
with st.spinner("Получаю данные..."):
data = {
"getter": "single_og",
"id": og_id,
"codes": codes,
"columns": columns
}
result, status = make_api_request("/svodka_pm/get_single_og", data)
result, status = make_api_request("/svodka_pm/get_data", data)
if status == 200:
st.success("✅ Данные получены")
@@ -201,11 +232,12 @@ def main():
if codes_total and columns_total:
with st.spinner("Получаю данные..."):
data = {
"getter": "total_ogs",
"codes": codes_total,
"columns": columns_total
}
result, status = make_api_request("/svodka_pm/get_total_ogs", data)
result, status = make_api_request("/svodka_pm/get_data", data)
if status == 200:
st.success("✅ Данные получены")
@@ -219,6 +251,9 @@ def main():
with tab2:
st.header("🏭 Сводки СА - Полный функционал")
# Получаем информацию о геттерах
getters_info = get_parser_getters("svodka_ca")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_ca = st.file_uploader(
@@ -246,7 +281,16 @@ def main():
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)
@@ -273,6 +317,7 @@ def main():
if modes and tables:
with st.spinner("Получаю данные..."):
data = {
"getter": "get_data",
"modes": modes,
"tables": tables
}
@@ -283,7 +328,7 @@ def main():
st.success("✅ Данные получены")
st.json(result)
else:
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
st.error(f"❌ Ошибка: {result.get('message', f'Неизвестная ошибка: {status}')}")
else:
st.warning("⚠️ Выберите режимы и таблицы")
@@ -291,6 +336,9 @@ def main():
with tab3:
st.header("⛽ Мониторинг топлива - Полный функционал")
# Получаем информацию о геттерах
getters_info = get_parser_getters("monitoring_fuel")
# Секция загрузки файлов
st.subheader("📤 Загрузка файлов")
uploaded_fuel = st.file_uploader(
@@ -319,6 +367,15 @@ def main():
# Секция получения данных
st.subheader("🔍 Получение данных")
# Показываем доступные геттеры
if getters_info and "getters" in getters_info:
st.info("📋 Доступные геттеры:")
for getter_name, getter_info in getters_info["getters"].items():
st.write(f"• **{getter_name}**: {getter_info.get('description', 'Нет описания')}")
st.write(f" - Обязательные параметры: {', '.join(getter_info.get('required_params', []))}")
if getter_info.get('optional_params'):
st.write(f" - Необязательные параметры: {', '.join(getter_info['optional_params'])}")
col1, col2 = st.columns(2)
with col1:
@@ -335,10 +392,11 @@ def main():
if columns_fuel:
with st.spinner("Получаю данные..."):
data = {
"getter": "total_by_columns",
"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:
st.success("✅ Данные получены")
@@ -360,10 +418,11 @@ def main():
if st.button("🔍 Получить данные за месяц", key="fuel_month_btn"):
with st.spinner("Получаю данные..."):
data = {
"getter": "month_by_code",
"month": month
}
result, status = make_api_request("/monitoring_fuel/get_month_by_code", data)
result, status = make_api_request("/monitoring_fuel/get_data", data)
if status == 200:
st.success("✅ Данные получены")
@@ -374,7 +433,7 @@ def main():
# Футер
st.markdown("---")
st.markdown("### 📚 Документация API")
st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs")
st.markdown(f"Полная документация доступна по адресу: {API_BASE_URL}/docs")
# Информация о проекте
with st.expander(" О проекте"):

View File

@@ -1,7 +1,4 @@
streamlit>=1.28.0
pandas>=2.0.0
requests>=2.31.0
pandas>=1.5.0
numpy>=1.24.0
plotly>=5.15.0
minio>=7.1.0
openpyxl>=3.1.0
xlrd>=2.0.1

84
test_api.py Normal file
View 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
View 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
View 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
View 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
View 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()