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/... → стенд по умолчанию
This commit is contained in:
+53
-6
@@ -1,7 +1,11 @@
|
||||
"""
|
||||
polygon v0.2.2 — Mock Nubes API.
|
||||
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>
|
||||
@@ -15,16 +19,16 @@ Blueprint'ы (каждый — отдельный файл в routes/):
|
||||
⛔ РОВНО 1 ВОРКЕР — состояние в памяти, не shared между процессами.
|
||||
|
||||
Переменные окружения:
|
||||
POLYGON_STANDS — список стендов: name:path,name:path,...
|
||||
MOCK_OP_DELAY — задержка операции в секундах (по умолчанию 0.1)
|
||||
WEB_CONCURRENCY — число gunicorn-воркеров (принудительно 1)
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from flask import Flask
|
||||
from flask import Flask, g, request
|
||||
|
||||
# ⛔ Принудительно 1 воркер — состояние в памяти, иначе инстансы теряются.
|
||||
# setdefault не перезапишет если Nubes/Procfile уже выставил WEB_CONCURRENCY=1.
|
||||
os.environ.setdefault("WEB_CONCURRENCY", "1")
|
||||
|
||||
# Импорт blueprint'ов — каждый отвечает за свою группу маршрутов
|
||||
@@ -36,14 +40,57 @@ 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/loader.py
|
||||
from config.loader import VERSION
|
||||
# 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'ы.
|
||||
# Префиксы уже заданы в url_prefix каждого blueprint'а (кроме root — он без префикса).
|
||||
app.register_blueprint(root_bp) # /health, /
|
||||
app.register_blueprint(services_bp) # /api/v1/svc/services
|
||||
app.register_blueprint(instances_bp) # /api/v1/svc/instances
|
||||
|
||||
Reference in New Issue
Block a user