From 8d4575530303f22e5eb1d07b182580b42804e9e4 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 23 Jan 2026 21:07:52 +0900 Subject: [PATCH] feat: init fastopenapi (#30453) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/app_factory.py | 2 + api/controllers/console/ping.py | 28 ++++++------ api/controllers/fastopenapi.py | 3 ++ api/extensions/ext_fastopenapi.py | 43 +++++++++++++++++++ api/pyproject.toml | 1 + api/pyrightconfig.json | 1 + .../console/test_fastopenapi_ping.py | 27 ++++++++++++ api/uv.lock | 19 ++++++++ 8 files changed, 110 insertions(+), 14 deletions(-) create mode 100644 api/controllers/fastopenapi.py create mode 100644 api/extensions/ext_fastopenapi.py create mode 100644 api/tests/unit_tests/controllers/console/test_fastopenapi_ping.py diff --git a/api/app_factory.py b/api/app_factory.py index 1fb01d2e91..07859a3758 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -81,6 +81,7 @@ def initialize_extensions(app: DifyApp): ext_commands, ext_compress, ext_database, + ext_fastopenapi, ext_forward_refs, ext_hosting_provider, ext_import_modules, @@ -128,6 +129,7 @@ def initialize_extensions(app: DifyApp): ext_proxy_fix, ext_blueprints, ext_commands, + ext_fastopenapi, ext_otel, ext_request_logging, ext_session_factory, diff --git a/api/controllers/console/ping.py b/api/controllers/console/ping.py index 25a3d80522..d480af312b 100644 --- a/api/controllers/console/ping.py +++ b/api/controllers/console/ping.py @@ -1,17 +1,17 @@ -from flask_restx import Resource, fields +from pydantic import BaseModel, Field -from . import console_ns +from controllers.fastopenapi import console_router -@console_ns.route("/ping") -class PingApi(Resource): - @console_ns.doc("health_check") - @console_ns.doc(description="Health check endpoint for connection testing") - @console_ns.response( - 200, - "Success", - console_ns.model("PingResponse", {"result": fields.String(description="Health check result", example="pong")}), - ) - def get(self): - """Health check endpoint for connection testing""" - return {"result": "pong"} +class PingResponse(BaseModel): + result: str = Field(description="Health check result", examples=["pong"]) + + +@console_router.get( + "/ping", + response_model=PingResponse, + tags=["console"], +) +def ping() -> PingResponse: + """Health check endpoint for connection testing.""" + return PingResponse(result="pong") diff --git a/api/controllers/fastopenapi.py b/api/controllers/fastopenapi.py new file mode 100644 index 0000000000..c13f22338b --- /dev/null +++ b/api/controllers/fastopenapi.py @@ -0,0 +1,3 @@ +from fastopenapi.routers import FlaskRouter + +console_router = FlaskRouter() diff --git a/api/extensions/ext_fastopenapi.py b/api/extensions/ext_fastopenapi.py new file mode 100644 index 0000000000..0ef1513e11 --- /dev/null +++ b/api/extensions/ext_fastopenapi.py @@ -0,0 +1,43 @@ +from fastopenapi.routers import FlaskRouter +from flask_cors import CORS + +from configs import dify_config +from controllers.fastopenapi import console_router +from dify_app import DifyApp +from extensions.ext_blueprints import AUTHENTICATED_HEADERS, EXPOSED_HEADERS + +DOCS_PREFIX = "/fastopenapi" + + +def init_app(app: DifyApp) -> None: + docs_enabled = dify_config.SWAGGER_UI_ENABLED + docs_url = f"{DOCS_PREFIX}/docs" if docs_enabled else None + redoc_url = f"{DOCS_PREFIX}/redoc" if docs_enabled else None + openapi_url = f"{DOCS_PREFIX}/openapi.json" if docs_enabled else None + + router = FlaskRouter( + app=app, + docs_url=docs_url, + redoc_url=redoc_url, + openapi_url=openapi_url, + openapi_version="3.0.0", + title="Dify API (FastOpenAPI PoC)", + version="1.0", + description="FastOpenAPI proof of concept for Dify API", + ) + + # Ensure route decorators are evaluated. + import controllers.console.ping as ping_module + + _ = ping_module + + router.include_router(console_router, prefix="/console/api") + CORS( + app, + resources={r"/console/api/*": {"origins": dify_config.CONSOLE_CORS_ALLOW_ORIGINS}}, + supports_credentials=True, + allow_headers=list(AUTHENTICATED_HEADERS), + methods=["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"], + expose_headers=list(EXPOSED_HEADERS), + ) + app.extensions["fastopenapi"] = router diff --git a/api/pyproject.toml b/api/pyproject.toml index d025a92846..141f6f0bb2 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -93,6 +93,7 @@ dependencies = [ "weaviate-client==4.17.0", "apscheduler>=3.11.0", "weave>=0.52.16", + "fastopenapi[flask]>=0.7.0", ] # Before adding new dependency, consider place it in # alphabet order (a-z) and suitable group. diff --git a/api/pyrightconfig.json b/api/pyrightconfig.json index 6a689b96df..007c49ddb0 100644 --- a/api/pyrightconfig.json +++ b/api/pyrightconfig.json @@ -8,6 +8,7 @@ ], "typeCheckingMode": "strict", "allowedUntypedLibraries": [ + "fastopenapi", "flask_restx", "flask_login", "opentelemetry.instrumentation.celery", diff --git a/api/tests/unit_tests/controllers/console/test_fastopenapi_ping.py b/api/tests/unit_tests/controllers/console/test_fastopenapi_ping.py new file mode 100644 index 0000000000..fc04ca078b --- /dev/null +++ b/api/tests/unit_tests/controllers/console/test_fastopenapi_ping.py @@ -0,0 +1,27 @@ +import builtins + +import pytest +from flask import Flask +from flask.views import MethodView + +from extensions import ext_fastopenapi + +if not hasattr(builtins, "MethodView"): + builtins.MethodView = MethodView # type: ignore[attr-defined] + + +@pytest.fixture +def app() -> Flask: + app = Flask(__name__) + app.config["TESTING"] = True + return app + + +def test_console_ping_fastopenapi_returns_pong(app: Flask): + ext_fastopenapi.init_app(app) + + client = app.test_client() + response = client.get("/console/api/ping") + + assert response.status_code == 200 + assert response.get_json() == {"result": "pong"} diff --git a/api/uv.lock b/api/uv.lock index 83aa89072c..bdbdc1f4f7 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1382,6 +1382,7 @@ dependencies = [ { name = "celery" }, { name = "charset-normalizer" }, { name = "croniter" }, + { name = "fastopenapi", extra = ["flask"] }, { name = "flask" }, { name = "flask-compress" }, { name = "flask-cors" }, @@ -1580,6 +1581,7 @@ requires-dist = [ { name = "celery", specifier = "~=5.5.2" }, { name = "charset-normalizer", specifier = ">=3.4.4" }, { name = "croniter", specifier = ">=6.0.0" }, + { name = "fastopenapi", extras = ["flask"], specifier = ">=0.7.0" }, { name = "flask", specifier = "~=3.1.2" }, { name = "flask-compress", specifier = ">=1.17,<1.18" }, { name = "flask-cors", specifier = "~=6.0.0" }, @@ -1921,6 +1923,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/93/aa8072af4ff37b795f6bbf43dcaf61115f40f49935c7dbb180c9afc3f421/fastapi-0.122.0-py3-none-any.whl", hash = "sha256:a456e8915dfc6c8914a50d9651133bd47ec96d331c5b44600baa635538a30d67", size = 110671 }, ] +[[package]] +name = "fastopenapi" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/02/6ee3ecc1e176bbb8c02cbeee30b11f526167c77ef2f7e741ab7999787ad0/fastopenapi-0.7.0.tar.gz", hash = "sha256:5a671fa663e3d89608e9b39a213595f7ac0bf0caf71f2b6016adf4d8c3e1a50e", size = 17191, upload-time = "2025-04-27T13:38:48.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/ad/1881ed46a0a7d3ce14472425db0ddc7aa45c858b4b14727647805487f10d/fastopenapi-0.7.0-py3-none-any.whl", hash = "sha256:482914301f348270cb231617863cacfadf1841012c5ff7d4255a27077704c7b2", size = 21272, upload-time = "2025-04-27T13:38:46.877Z" }, +] + +[package.optional-dependencies] +flask = [ + { name = "flask" }, +] + [[package]] name = "fastuuid" version = "0.14.0"