""" polygon v0.5.0 — Mock Nubes API. Эмулятор REST API облачной платформы Nubes для интеграционных тестов. Поддержка нескольких стендов: URL: //api/v1/svc/... (stand_id из POLYGON_STANDS) Состояние изолировано: у каждого стенда свой набор инстансов и операций. Blueprint'ы (каждый — отдельный файл в routes/): - root_bp → /health, / (HTML-страница) - services_bp → /api/v1/svc/services, /services/ - 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//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//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)