6 Commits

Author SHA1 Message Date
72fe115a99 main 2025-09-01 19:22:58 +03:00
46a30c32ed Сервисы 2025-09-01 19:20:16 +03:00
5e217c7cce порты 2025-09-01 19:19:28 +03:00
7d2747c8fe rm лишнее 2025-09-01 19:16:23 +03:00
513ff3c144 Реализация для дева с хот релоадом 2025-09-01 19:06:55 +03:00
a0b6e04d99 Правильное отображение имени 2025-09-01 19:01:06 +03:00
10 changed files with 402 additions and 116 deletions

View File

@@ -1,39 +0,0 @@
# 🧹 Сводка по очистке проекта
## ✅ Что было удалено из `python_parser/`:
### Файлы Streamlit:
- `streamlit_app.py` - основной файл Streamlit приложения
- `run_streamlit.py` - скрипт запуска Streamlit
- `Procfile` - конфигурация для Heroku (Streamlit)
- `runtime.txt` - версия Python для Heroku
- `manifest.yml` - манифест приложения
- `.streamlit/` - папка с конфигурацией Streamlit
### Зависимости:
- Удален `streamlit>=1.28.0` из `python_parser/requirements.txt`
## 🎯 Результат:
### `python_parser/` - теперь содержит ТОЛЬКО:
- FastAPI приложение
- Адаптеры для парсеров
- Основную бизнес-логику
- Dockerfile для FastAPI
- Зависимости только для FastAPI
### `streamlit_app/` - содержит ТОЛЬКО:
- Streamlit приложение
- Dockerfile для Streamlit
- Зависимости только для Streamlit
- Конфигурацию Streamlit
## 🔄 Полное разделение достигнуто:
- **FastAPI** и **Streamlit** теперь полностью независимы
- Каждый сервис имеет свои собственные зависимости
- Docker образы собираются отдельно
- Запускаются через единый `docker-compose.yml`
---
**Статус**: ✅ Проект полностью очищен и разделен

View File

@@ -11,11 +11,25 @@
- Docker и Docker Compose - Docker и Docker Compose
- Git - Git
### Запуск всех сервисов ### Запуск всех сервисов (продакшн)
```bash ```bash
docker-compose up -d docker compose up -d
``` ```
### Запуск в режиме разработки
```bash
# Автоматический запуск
python start_dev.py
# Или вручную
docker compose -f docker-compose.dev.yml up -d
```
**Режим разработки** позволяет:
- Автоматически перезагружать Streamlit при изменении кода
- Монтировать исходный код напрямую в контейнер
- Видеть изменения без пересборки контейнеров
### Доступ к сервисам ### Доступ к сервисам
- **FastAPI**: http://localhost:8000 - **FastAPI**: http://localhost:8000
- **Streamlit**: http://localhost:8501 - **Streamlit**: http://localhost:8501
@@ -72,6 +86,18 @@ python_parser_cf/
## 🛠️ Разработка ## 🛠️ Разработка
### Режим разработки (рекомендуется)
```bash
# Запуск режима разработки
python start_dev.py
# Остановка
docker compose -f docker-compose.dev.yml down
# Возврат к продакшн режиму
python start_prod.py
```
### Локальная разработка FastAPI ### Локальная разработка FastAPI
```bash ```bash
cd python_parser cd python_parser

58
docker-compose.dev.yml Normal file
View File

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

View File

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

View File

@@ -96,6 +96,54 @@ async def get_available_parsers():
return {"parsers": parsers} return {"parsers": parsers}
@app.get("/parsers/{parser_name}/getters", tags=["Общее"],
summary="Информация о геттерах парсера",
description="Возвращает информацию о доступных геттерах для указанного парсера",
responses={
200: {
"content": {
"application/json": {
"example": {
"parser": "svodka_pm",
"getters": {
"single_og": {
"required_params": ["id", "codes", "columns"],
"optional_params": ["search"],
"description": "Получение данных по одному ОГ"
},
"total_ogs": {
"required_params": ["codes", "columns"],
"optional_params": ["search"],
"description": "Получение данных по всем ОГ"
}
}
}
}
}
},
404: {
"description": "Парсер не найден"
}
})
async def get_parser_getters(parser_name: str):
"""Получение информации о геттерах парсера"""
if parser_name not in PARSERS:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Парсер '{parser_name}' не найден"
)
parser_class = PARSERS[parser_name]
parser_instance = parser_class()
getters_info = parser_instance.get_available_getters()
return {
"parser": parser_name,
"getters": getters_info
}
@app.get("/server-info", tags=["Общее"], @app.get("/server-info", tags=["Общее"],
summary="Информация о сервере", summary="Информация о сервере",
response_model=ServerInfoResponse,) response_model=ServerInfoResponse,)
@@ -352,40 +400,40 @@ async def get_svodka_pm_total_ogs(
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
# @app.post("/svodka_pm/get_data", tags=[SvodkaPMParser.name]) @app.post("/svodka_pm/get_data", tags=[SvodkaPMParser.name])
# async def get_svodka_pm_data( async def get_svodka_pm_data(
# request_data: dict request_data: dict
# ): ):
# report_service = get_report_service() report_service = get_report_service()
# """ """
# Получение данных из отчета сводки факта СарНПЗ Получение данных из отчета сводки факта СарНПЗ
# - indicator_id: ID индикатора - indicator_id: ID индикатора
# - code: Код для поиска - code: Код для поиска
# - search_value: Опциональное значение для поиска - search_value: Опциональное значение для поиска
# """ """
# try: try:
# # Создаем запрос # Создаем запрос
# request = DataRequest( request = DataRequest(
# report_type='svodka_pm', report_type='svodka_pm',
# get_params=request_data get_params=request_data
# ) )
# # Получаем данные # Получаем данные
# result = report_service.get_data(request) result = report_service.get_data(request)
# if result.success: if result.success:
# return { return {
# "success": True, "success": True,
# "data": result.data "data": result.data
# } }
# else: else:
# raise HTTPException(status_code=404, detail=result.message) raise HTTPException(status_code=404, detail=result.message)
# except HTTPException: except HTTPException:
# raise raise
# except Exception as e: except Exception as e:
# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
@app.post("/svodka_ca/upload", tags=[SvodkaCAParser.name], @app.post("/svodka_ca/upload", tags=[SvodkaCAParser.name],
@@ -562,38 +610,38 @@ async def get_svodka_ca_data(
# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") # raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
# @app.post("/monitoring_fuel/get_data", tags=[MonitoringFuelParser.name]) @app.post("/monitoring_fuel/get_data", tags=[MonitoringFuelParser.name])
# async def get_monitoring_fuel_data( async def get_monitoring_fuel_data(
# request_data: dict request_data: dict
# ): ):
# report_service = get_report_service() report_service = get_report_service()
# """ """
# Получение данных из отчета мониторинга топлива Получение данных из отчета мониторинга топлива
# - column: Название колонки для агрегации (normativ, total, total_svod) - column: Название колонки для агрегации (normativ, total, total_svod)
# """ """
# try: try:
# # Создаем запрос # Создаем запрос
# request = DataRequest( request = DataRequest(
# report_type='monitoring_fuel', report_type='monitoring_fuel',
# get_params=request_data get_params=request_data
# ) )
# # Получаем данные # Получаем данные
# result = report_service.get_data(request) result = report_service.get_data(request)
# if result.success: if result.success:
# return { return {
# "success": True, "success": True,
# "data": result.data "data": result.data
# } }
# else: else:
# raise HTTPException(status_code=404, detail=result.message) raise HTTPException(status_code=404, detail=result.message)
# except HTTPException: except HTTPException:
# raise raise
# except Exception as e: except Exception as e:
# raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}") raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
# @app.post("/monitoring_fuel/upload_directory", tags=[MonitoringFuelParser.name]) # @app.post("/monitoring_fuel/upload_directory", tags=[MonitoringFuelParser.name])

View File

@@ -2,28 +2,93 @@
Порты (интерфейсы) для hexagonal architecture Порты (интерфейсы) для hexagonal architecture
""" """
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Optional from typing import Optional, Dict, List, Any, Callable
import pandas as pd import pandas as pd
class ParserPort(ABC): class ParserPort(ABC):
"""Интерфейс для парсеров""" """Интерфейс для парсеров с поддержкой множественных геттеров"""
def __init__(self):
"""Инициализация с пустым словарем геттеров"""
self.getters: Dict[str, Dict[str, Any]] = {}
self._register_default_getters()
def _register_default_getters(self):
"""Регистрация геттеров по умолчанию - переопределяется в наследниках"""
pass
def register_getter(self, name: str, method: Callable, required_params: List[str],
optional_params: List[str] = None, description: str = ""):
"""
Регистрация нового геттера
Args:
name: Имя геттера
method: Метод для выполнения
required_params: Список обязательных параметров
optional_params: Список необязательных параметров
description: Описание геттера
"""
if optional_params is None:
optional_params = []
self.getters[name] = {
"method": method,
"required_params": required_params,
"optional_params": optional_params,
"description": description
}
def get_available_getters(self) -> Dict[str, Dict[str, Any]]:
"""Получение списка доступных геттеров с их описанием"""
return {
name: {
"required_params": info["required_params"],
"optional_params": info["optional_params"],
"description": info["description"]
}
for name, info in self.getters.items()
}
# Добавить схему
def get_value(self, getter_name: str, params: Dict[str, Any]):
"""
Получение значения через указанный геттер
Args:
getter_name: Имя геттера
params: Параметры для геттера
Returns:
Результат выполнения геттера
Raises:
ValueError: Если геттер не найден или параметры неверны
"""
if getter_name not in self.getters:
available = list(self.getters.keys())
raise ValueError(f"Геттер '{getter_name}' не найден. Доступные: {available}")
getter_info = self.getters[getter_name]
required = getter_info["required_params"]
# Проверка обязательных параметров
missing = [p for p in required if p not in params]
if missing:
raise ValueError(f"Отсутствуют обязательные параметры для геттера '{getter_name}': {missing}")
# Вызов метода геттера
try:
return getter_info["method"](params)
except Exception as e:
raise ValueError(f"Ошибка выполнения геттера '{getter_name}': {str(e)}")
@abstractmethod @abstractmethod
def parse(self, file_path: str, params: dict) -> pd.DataFrame: def parse(self, file_path: str, params: dict) -> pd.DataFrame:
"""Парсинг файла и возврат DataFrame""" """Парсинг файла и возврат DataFrame"""
pass pass
@abstractmethod
def get_value(self, df: pd.DataFrame, params: dict):
"""Получение значения из DataFrame по параметрам"""
pass
# @abstractmethod
# def get_schema(self) -> dict:
# """Возвращает схему входных параметров для парсера"""
# pass
class StoragePort(ABC): class StoragePort(ABC):
"""Интерфейс для хранилища данных""" """Интерфейс для хранилища данных"""

View File

@@ -100,8 +100,34 @@ class ReportService:
# Получаем парсер # Получаем парсер
parser = get_parser(request.report_type) parser = get_parser(request.report_type)
# Получаем значение # Устанавливаем DataFrame в парсер для использования в геттерах
value = parser.get_value(df, request.get_params) parser.df = df
# Получаем параметры запроса
get_params = request.get_params or {}
# Определяем имя геттера (по умолчанию используем первый доступный)
getter_name = get_params.pop("getter", None)
if not getter_name:
# Если геттер не указан, берем первый доступный
available_getters = list(parser.getters.keys())
if available_getters:
getter_name = available_getters[0]
print(f"⚠️ Геттер не указан, используем первый доступный: {getter_name}")
else:
return DataResult(
success=False,
message="Парсер не имеет доступных геттеров"
)
# Получаем значение через указанный геттер
try:
value = parser.get_value(getter_name, get_params)
except ValueError as e:
return DataResult(
success=False,
message=f"Ошибка параметров: {str(e)}"
)
# Формируем результат # Формируем результат
if value is not None: if value is not None:

49
start_dev.py Normal file
View File

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

49
start_prod.py Normal file
View File

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

View File

@@ -16,7 +16,8 @@ st.set_page_config(
) )
# Конфигурация API # Конфигурация API
API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") API_BASE_URL = os.getenv("API_BASE_URL", "http://fastapi:8000") # Внутренний адрес для Docker
API_PUBLIC_URL = os.getenv("API_PUBLIC_URL", "http://localhost:8000") # Внешний адрес для пользователя
def check_api_health(): def check_api_health():
"""Проверка доступности API""" """Проверка доступности API"""
@@ -73,7 +74,7 @@ def main():
st.info("Убедитесь, что FastAPI сервер запущен") st.info("Убедитесь, что FastAPI сервер запущен")
return return
st.success(f"✅ API доступен по адресу {API_BASE_URL}") st.success(f"✅ API доступен по адресу {API_PUBLIC_URL}")
# Боковая панель с информацией # Боковая панель с информацией
with st.sidebar: with st.sidebar:
@@ -373,7 +374,7 @@ def main():
# Футер # Футер
st.markdown("---") st.markdown("---")
st.markdown("### 📚 Документация API") st.markdown("### 📚 Документация API")
st.markdown(f"Полная документация доступна по адресу: {API_BASE_URL}/docs") st.markdown(f"Полная документация доступна по адресу: {API_PUBLIC_URL}/docs")
# Информация о проекте # Информация о проекте
with st.expander(" О проекте"): with st.expander(" О проекте"):