mirror of
https://github.com/langgenius/dify.git
synced 2026-05-01 10:00:11 -04:00
173 lines
5.5 KiB
Python
173 lines
5.5 KiB
Python
"""Generate Flask-RESTX Swagger 2.0 specs without booting the full backend.
|
|
|
|
This helper intentionally avoids `app_factory.create_app()`. The normal backend
|
|
startup eagerly initializes database, Redis, Celery, and storage extensions,
|
|
which is unnecessary when the goal is only to serialize the Flask-RESTX
|
|
`/swagger.json` documents.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from flask import Flask
|
|
from flask_restx.swagger import Swagger
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
API_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(API_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(API_ROOT))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SpecTarget:
|
|
route: str
|
|
filename: str
|
|
|
|
|
|
SPEC_TARGETS: tuple[SpecTarget, ...] = (
|
|
SpecTarget(route="/console/api/swagger.json", filename="console-swagger.json"),
|
|
SpecTarget(route="/api/swagger.json", filename="web-swagger.json"),
|
|
SpecTarget(route="/v1/swagger.json", filename="service-swagger.json"),
|
|
)
|
|
|
|
_ORIGINAL_REGISTER_MODEL = Swagger.register_model
|
|
_ORIGINAL_REGISTER_FIELD = Swagger.register_field
|
|
|
|
|
|
def _apply_runtime_defaults() -> None:
|
|
"""Force the small config surface required for Swagger generation."""
|
|
|
|
os.environ.setdefault("SECRET_KEY", "spec-export")
|
|
os.environ.setdefault("STORAGE_TYPE", "local")
|
|
os.environ.setdefault("STORAGE_LOCAL_PATH", "/tmp/dify-storage")
|
|
os.environ.setdefault("SWAGGER_UI_ENABLED", "true")
|
|
|
|
from configs import dify_config
|
|
|
|
dify_config.SECRET_KEY = os.environ["SECRET_KEY"]
|
|
dify_config.STORAGE_TYPE = "local"
|
|
dify_config.STORAGE_LOCAL_PATH = os.environ["STORAGE_LOCAL_PATH"]
|
|
dify_config.SWAGGER_UI_ENABLED = os.environ["SWAGGER_UI_ENABLED"].lower() == "true"
|
|
|
|
|
|
def _patch_swagger_for_inline_nested_dicts() -> None:
|
|
"""Teach Flask-RESTX Swagger generation to tolerate inline nested field maps.
|
|
|
|
Some existing controllers use `fields.Nested({...})` with a raw field mapping
|
|
instead of a named `api.model(...)`. Flask-RESTX crashes on those anonymous
|
|
dicts during schema registration, so this helper upgrades them into temporary
|
|
named models at export time.
|
|
"""
|
|
|
|
if getattr(Swagger, "_dify_inline_nested_dict_patch", False):
|
|
return
|
|
|
|
def get_or_create_inline_model(self: Swagger, nested_fields: dict[object, object]) -> object:
|
|
anonymous_models = getattr(self, "_anonymous_inline_models", None)
|
|
if anonymous_models is None:
|
|
anonymous_models = {}
|
|
self._anonymous_inline_models = anonymous_models
|
|
|
|
anonymous_name = anonymous_models.get(id(nested_fields))
|
|
if anonymous_name is None:
|
|
anonymous_name = f"_AnonymousInlineModel{len(anonymous_models) + 1}"
|
|
anonymous_models[id(nested_fields)] = anonymous_name
|
|
self.api.model(anonymous_name, nested_fields)
|
|
|
|
return self.api.models[anonymous_name]
|
|
|
|
def register_model_with_inline_dict_support(self: Swagger, model: object) -> dict[str, str]:
|
|
if isinstance(model, dict):
|
|
model = get_or_create_inline_model(self, model)
|
|
|
|
return _ORIGINAL_REGISTER_MODEL(self, model)
|
|
|
|
def register_field_with_inline_dict_support(self: Swagger, field: object) -> None:
|
|
nested = getattr(field, "nested", None)
|
|
if isinstance(nested, dict):
|
|
field.model = get_or_create_inline_model(self, nested) # type: ignore
|
|
|
|
_ORIGINAL_REGISTER_FIELD(self, field)
|
|
|
|
Swagger.register_model = register_model_with_inline_dict_support
|
|
Swagger.register_field = register_field_with_inline_dict_support
|
|
Swagger._dify_inline_nested_dict_patch = True
|
|
|
|
|
|
def create_spec_app() -> Flask:
|
|
"""Build a minimal Flask app that only mounts the Swagger-producing blueprints."""
|
|
|
|
_apply_runtime_defaults()
|
|
_patch_swagger_for_inline_nested_dicts()
|
|
|
|
app = Flask(__name__)
|
|
|
|
from controllers.console import bp as console_bp
|
|
from controllers.service_api import bp as service_api_bp
|
|
from controllers.web import bp as web_bp
|
|
|
|
app.register_blueprint(console_bp)
|
|
app.register_blueprint(web_bp)
|
|
app.register_blueprint(service_api_bp)
|
|
|
|
return app
|
|
|
|
|
|
def generate_specs(output_dir: Path) -> list[Path]:
|
|
"""Write all Swagger specs to `output_dir` and return the written paths."""
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
app = create_spec_app()
|
|
client = app.test_client()
|
|
|
|
written_paths: list[Path] = []
|
|
for target in SPEC_TARGETS:
|
|
response = client.get(target.route)
|
|
if response.status_code != 200:
|
|
raise RuntimeError(f"failed to fetch {target.route}: {response.status_code}")
|
|
|
|
payload = response.get_json()
|
|
if not isinstance(payload, dict):
|
|
raise RuntimeError(f"unexpected response payload for {target.route}")
|
|
|
|
output_path = output_dir / target.filename
|
|
output_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
written_paths.append(output_path)
|
|
|
|
return written_paths
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--output-dir",
|
|
type=Path,
|
|
default=Path("openapi"),
|
|
help="Directory where the Swagger JSON files will be written.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
written_paths = generate_specs(args.output_dir)
|
|
|
|
for path in written_paths:
|
|
logger.debug(path)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|