Files
polygon/site/app.py
T
naeel 58d54f2a95 v0.5.0: 3 стенда — изоляция состояния, WSGI middleware, OpenAPI server selector, POLYGON_STANDS env
- loader.py: STANDS dict, get_services/get_ops_index per stand, POLYGON_STANDS env
- mock_state.py: get_state(stand_id) — ленивая инициализация
- app.py: StandMiddleware — перезапись PATH_INFO до Flask-роутинга
- Все роуты: LocalProxy(state), LocalProxy(SERVICES/OPS_INDEX) через g.stand_id
- openapi.py: _build_servers() — по одному серверу на стенд
- services/stand{1,2,3}/ — 37 YAML на стенд из STANDS/dev и STANDS/test
- tests/test_api.py: +4 multi-stand теста (изоляция, fail-next, OpenAPI servers)
- Обратная совместимость: /api/v1/svc/... → стенд по умолчанию
2026-08-01 20:10:19 +04:00

106 lines
4.9 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.
"""
polygon v0.5.0 — Mock Nubes API.
Эмулятор REST API облачной платформы Nubes для интеграционных тестов.
Поддержка нескольких стендов:
URL: /<stand_id>/api/v1/svc/... (stand_id из POLYGON_STANDS)
Состояние изолировано: у каждого стенда свой набор инстансов и операций.
Blueprint'ы (каждый — отдельный файл в routes/):
- root_bp → /health, / (HTML-страница)
- services_bp → /api/v1/svc/services, /services/<id>
- instances_bp → /api/v1/svc/instances (GET list, GET detail, POST create)
- operations_bp → /api/v1/svc/instanceOperations/* (create, default, status, params, validate)
- run_bp → /api/v1/svc/instanceOperations/<uid>/run
- mock_bp → /api/v1/svc/_mock/* (reset, state, services, delay)
Деплой: Nubes pythonk8s (managed service) с gunicorn:
gunicorn app:app --workers 1 --bind 0.0.0.0:8000
⛔ РОВНО 1 ВОРКЕР — состояние в памяти, не shared между процессами.
Переменные окружения:
POLYGON_STANDS — список стендов: name:path,name:path,...
MOCK_OP_DELAY — задержка операции в секундах (по умолчанию 0.1)
WEB_CONCURRENCY — число gunicorn-воркеров (принудительно 1)
"""
import os
from flask import Flask, g, request
# ⛔ Принудительно 1 воркер — состояние в памяти, иначе инстансы теряются.
os.environ.setdefault("WEB_CONCURRENCY", "1")
# Импорт blueprint'ов — каждый отвечает за свою группу маршрутов
from routes.root import bp as root_bp
from routes.services_routes import bp as services_bp
from routes.instances_routes import bp as instances_bp
from routes.operations_routes import bp as operations_bp
from routes.run import bp as run_bp
from routes.mock_routes import bp as mock_bp
from routes.openapi import bp as openapi_bp
# config
import config.loader as _cfg
# Flask-приложение с Jinja2-шаблонами и статикой
app = Flask(__name__, template_folder="templates", static_folder="static")
# ═══════════════════════════════════════════════════════════════
# WSGI Middleware — перезапись PATH_INFO до Flask-роутинга
# ═══════════════════════════════════════════════════════════════
class StandMiddleware:
"""WSGI middleware: извлекает stand_id из URL и перезаписывает PATH_INFO.
/stand1/api/v1/svc/... → environ['stand_id']='stand1', PATH_INFO=/api/v1/svc/...
/api/v1/svc/... → environ['stand_id']=DEFAULT_STAND, без изменений
/health, /swagger, / → environ['stand_id']=DEFAULT_STAND, без изменений
Должен быть ДО Flask-роутинга, поэтому это middleware, а не before_request.
"""
def __init__(self, wsgi_app):
self.wsgi_app = wsgi_app
def __call__(self, environ, start_response):
path = environ.get("PATH_INFO", "/")
parts = path.split("/")
if len(parts) > 1 and parts[1] in _cfg.STANDS:
environ["polygon.stand_id"] = parts[1]
new_path = "/" + "/".join(parts[2:]) if len(parts) > 2 else "/"
environ["PATH_INFO"] = new_path
else:
environ["polygon.stand_id"] = _cfg.DEFAULT_STAND
return self.wsgi_app(environ, start_response)
app.wsgi_app = StandMiddleware(app.wsgi_app)
@app.before_request
def _set_stand():
"""Копирует stand_id из environ в g."""
g.stand_id = request.environ.get("polygon.stand_id", _cfg.DEFAULT_STAND)
# ═══════════════════════════════════════════════════════════════
# Регистрируем blueprint'ы.
app.register_blueprint(root_bp) # /health, /
app.register_blueprint(services_bp) # /api/v1/svc/services
app.register_blueprint(instances_bp) # /api/v1/svc/instances
app.register_blueprint(operations_bp) # /api/v1/svc/instanceOperations + /instanceOperationCfsParams
app.register_blueprint(run_bp) # /api/v1/svc/instanceOperations/<uid>/run
app.register_blueprint(mock_bp) # /api/v1/svc/_mock/*
app.register_blueprint(openapi_bp) # /api/v1/svc/openapi.json
# Локальный запуск (без gunicorn) — только для разработки
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000)