klg-asutk-app/backend/app/streaming/risingwave.py
Yuriy 0150aba4f5 Consolidation: KLG ASUTK + PAPA integration
- Unify API: lib/api.ts uses /api/v1, inbox uses /api/inbox (rewrites)
- Remove localhost refs: openapi, inbox page
- Add rewrites: /api/inbox|tmc -> inbox-server, /api/v1 -> FastAPI
- Add stub routes: knowledge/insights, recommendations, search, log-error
- Transfer from PAPA: prompts (inspection, tmc), scripts, supabase, data/tmc-requests
- Fix inbox-server: ORDER BY created_at, package.json
- Remove redundant app/api/inbox/files route (rewrites handle it)
- knowledge/ in gitignore (large PDFs)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 17:18:31 +03:00

74 lines
2.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
RisingWave интеграция
"""
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text
from app.core.config import settings
import logging
import re
logger = logging.getLogger(__name__)
engine = None
SessionLocal = None
# Whitelist допустимых имён materialized views
ALLOWED_VIEWS = {
"mv_aircraft_status",
"mv_audit_summary",
"mv_risk_alerts",
"mv_compliance_stats",
"mv_airworthiness_overview",
"mv_notifications_recent",
}
async def init_risingwave():
"""Инициализация RisingWave (только при ENABLE_RISINGWAVE=true)"""
from app.core.config import settings
if not settings.ENABLE_RISINGWAVE:
raise NotImplementedError("RisingWave отключён для MVP. Установите ENABLE_RISINGWAVE=true.")
global engine, SessionLocal
engine = create_async_engine(
settings.RISINGWAVE_URL.replace('postgresql://', 'postgresql+asyncpg://'),
echo=False,
)
SessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
logger.info("RisingWave connection pool created")
def _validate_view_name(view_name: str) -> str:
"""Валидация имени view против whitelist и формата"""
if view_name not in ALLOWED_VIEWS:
raise ValueError(f"View '{view_name}' is not in the allowed list. Allowed: {ALLOWED_VIEWS}")
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', view_name):
raise ValueError(f"Invalid view name format: '{view_name}'")
return view_name
async def query_materialized_view(view_name: str, filters: dict = None):
"""Запрос к materialized view (с защитой от SQL injection)"""
if not settings.ENABLE_RISINGWAVE:
raise NotImplementedError("RisingWave отключён для MVP.")
if not SessionLocal:
await init_risingwave()
safe_view = _validate_view_name(view_name)
async with SessionLocal() as session:
if filters:
conditions = " AND ".join([f"{k} = :{k}" for k in filters.keys()])
query = text(f"SELECT * FROM {safe_view} WHERE {conditions}")
result = await session.execute(query, filters)
else:
query = text(f"SELECT * FROM {safe_view}")
result = await session.execute(query)
return result.fetchall()