This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new ba0d3cd7733 Support root middleware from plugins (#48678)
ba0d3cd7733 is described below

commit ba0d3cd77331d27f39d7e69748c8a0865daf9b87
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Fri Apr 4 08:07:16 2025 +0200

    Support root middleware from plugins (#48678)
    
    * Add support for root middleware from plugins
    
    * Update generated airflow-ctl datamodels
    
    * Fix CI
    
    * Update airflow-ctl/pyproject.toml
    
    * Address comments and Fix CI
    
    * Address comments
    
    * Update airflow-core/docs/administration-and-deployment/plugins.rst
    
    Co-authored-by: Kalyan R <[email protected]>
    
    * Update airflow-core/docs/administration-and-deployment/plugins.rst
    
    Co-authored-by: Kalyan R <[email protected]>
    
    ---------
    
    Co-authored-by: Kalyan R <[email protected]>
---
 .../docs/administration-and-deployment/plugins.rst | 15 ++++++++-
 airflow-core/src/airflow/api_fastapi/app.py        | 38 ++++++++++++++++++++--
 .../src/airflow/api_fastapi/core_api/app.py        | 23 -------------
 .../api_fastapi/core_api/datamodels/plugins.py     | 10 ++++++
 .../api_fastapi/core_api/openapi/v1-generated.yaml | 21 ++++++++++++
 airflow-core/src/airflow/plugins_manager.py        | 20 ++++++++++--
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 26 +++++++++++++++
 .../airflow/ui/openapi-gen/requests/types.gen.ts   | 10 ++++++
 .../unit/cli/commands/test_plugins_command.py      |  6 ++++
 airflow-core/tests/unit/plugins/test_plugin.py     | 15 +++++++++
 .../src/airflowctl/api/datamodels/generated.py     | 19 +++++++++++
 .../tests/airflow_ctl/api/test_operations.py       |  4 +++
 .../src/tests_common/test_utils/mock_plugins.py    |  1 +
 13 files changed, 180 insertions(+), 28 deletions(-)

diff --git a/airflow-core/docs/administration-and-deployment/plugins.rst 
b/airflow-core/docs/administration-and-deployment/plugins.rst
index 7f2ef7d4a7a..47c0f2fecdf 100644
--- a/airflow-core/docs/administration-and-deployment/plugins.rst
+++ b/airflow-core/docs/administration-and-deployment/plugins.rst
@@ -106,8 +106,10 @@ looks like:
         macros = []
         # A list of Blueprint object created from flask.Blueprint. For use 
with the flask_appbuilder based GUI
         flask_blueprints = []
-        # A list of dictionaries contanning FastAPI object and some metadata. 
See example below.
+        # A list of dictionaries containing FastAPI app objects and some 
metadata. See the example below.
         fastapi_apps = []
+        # A list of dictionaries containing FastAPI middleware factory objects 
and some metadata. See the example below.
+        fastapi_root_middlewares = []
         # A list of dictionaries containing FlaskAppBuilder BaseView object 
and some metadata. See example below
         appbuilder_views = []
         # A list of dictionaries containing kwargs for FlaskAppBuilder 
add_link. See example below
@@ -166,6 +168,7 @@ definitions in Airflow.
     from airflow.providers.fab.www.auth import has_access
 
     from fastapi import FastAPI
+    from fastapi.middleware.trustedhost import TrustedHostMiddleware
     from flask import Blueprint
     from flask_appbuilder import expose, BaseView as AppBuilderBaseView
 
@@ -201,6 +204,15 @@ definitions in Airflow.
     app_with_metadata = {"app": app, "url_prefix": "/some_prefix", "name": 
"Name of the App"}
 
 
+    # Creating a FastAPI middleware that will operates on all the server api 
requests.
+    middleware_with_metadata = {
+        "middleware": TrustedHostMiddleware,
+        "args": [],
+        "kwargs": {"allowed_hosts": ["example.com", "*.example.com"]},
+        "name": "Name of the Middleware",
+    }
+
+
     # Creating a flask appbuilder BaseView
     class TestAppBuilderBaseView(AppBuilderBaseView):
         default_view = "test"
@@ -257,6 +269,7 @@ definitions in Airflow.
         macros = [plugin_macro]
         flask_blueprints = [bp]
         fastapi_apps = [app_with_metadata]
+        fastapi_root_middlewares = [middleware_with_metadata]
         appbuilder_views = [v_appbuilder_package, v_appbuilder_nomenu_package]
         appbuilder_menu_items = [appbuilder_mitem, appbuilder_mitem_toplevel]
 
diff --git a/airflow-core/src/airflow/api_fastapi/app.py 
b/airflow-core/src/airflow/api_fastapi/app.py
index 4ef7ab65113..28c66cabcfc 100644
--- a/airflow-core/src/airflow/api_fastapi/app.py
+++ b/airflow-core/src/airflow/api_fastapi/app.py
@@ -19,7 +19,7 @@ from __future__ import annotations
 import logging
 import os
 from contextlib import AsyncExitStack, asynccontextmanager
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, cast
 from urllib.parse import urlsplit
 
 from fastapi import FastAPI
@@ -31,7 +31,6 @@ from airflow.api_fastapi.core_api.app import (
     init_error_handlers,
     init_flask_plugins,
     init_middlewares,
-    init_plugins,
     init_views,
 )
 from airflow.api_fastapi.execution_api.app import create_task_execution_api_app
@@ -160,3 +159,38 @@ def get_auth_manager() -> BaseAuthManager:
             "The `init_auth_manager` method needs to be called first."
         )
     return auth_manager
+
+
+def init_plugins(app: FastAPI) -> None:
+    """Integrate FastAPI app and middleware plugins."""
+    from airflow import plugins_manager
+
+    plugins_manager.initialize_fastapi_plugins()
+
+    # After calling initialize_fastapi_plugins, fastapi_apps cannot be None 
anymore.
+    for subapp_dict in cast("list", plugins_manager.fastapi_apps):
+        name = subapp_dict.get("name")
+        subapp = subapp_dict.get("app")
+        if subapp is None:
+            log.error("'app' key is missing for the fastapi app: %s", name)
+            continue
+        url_prefix = subapp_dict.get("url_prefix")
+        if url_prefix is None:
+            log.error("'url_prefix' key is missing for the fastapi app: %s", 
name)
+            continue
+
+        log.debug("Adding subapplication %s under prefix %s", name, url_prefix)
+        app.mount(url_prefix, subapp)
+
+    for middleware_dict in cast("list", 
plugins_manager.fastapi_root_middlewares):
+        name = middleware_dict.get("name")
+        middleware = middleware_dict.get("middleware")
+        args = middleware_dict.get("args", [])
+        kwargs = middleware_dict.get("kwargs", {})
+
+        if middleware is None:
+            log.error("'middleware' key is missing for the fastapi middleware: 
%s", name)
+            continue
+
+        log.debug("Adding root middleware %s", name)
+        app.add_middleware(middleware, *args, **kwargs)
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/app.py 
b/airflow-core/src/airflow/api_fastapi/core_api/app.py
index 9e62aaeae36..22ac4aba56e 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/app.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/app.py
@@ -20,7 +20,6 @@ import logging
 import os
 import warnings
 from pathlib import Path
-from typing import cast
 
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
@@ -85,28 +84,6 @@ def init_views(app: FastAPI) -> None:
         )
 
 
-def init_plugins(app: FastAPI) -> None:
-    """Integrate FastAPI app plugins."""
-    from airflow import plugins_manager
-
-    plugins_manager.initialize_fastapi_plugins()
-
-    # After calling initialize_fastapi_plugins, fastapi_apps cannot be None 
anymore.
-    for subapp_dict in cast("list", plugins_manager.fastapi_apps):
-        name = subapp_dict.get("name")
-        subapp = subapp_dict.get("app")
-        if subapp is None:
-            log.error("'app' key is missing for the fastapi app: %s", name)
-            continue
-        url_prefix = subapp_dict.get("url_prefix")
-        if url_prefix is None:
-            log.error("'url_prefix' key is missing for the fastapi app: %s", 
name)
-            continue
-
-        log.debug("Adding subapplication %s under prefix %s", name, url_prefix)
-        app.mount(url_prefix, subapp)
-
-
 def init_flask_plugins(app: FastAPI) -> None:
     """Integrate Flask plugins (plugins from Airflow 2)."""
     from airflow import plugins_manager
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py
index d36e5faf851..1aab230ac3d 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py
@@ -39,6 +39,15 @@ class FastAPIAppResponse(BaseModel):
     name: str
 
 
+class FastAPIRootMiddlewareResponse(BaseModel):
+    """Serializer for Plugin FastAPI root middleware responses."""
+
+    model_config = ConfigDict(extra="allow")
+
+    middleware: str
+    name: str
+
+
 class AppBuilderViewResponse(BaseModel):
     """Serializer for AppBuilder View responses."""
 
@@ -67,6 +76,7 @@ class PluginResponse(BaseModel):
     macros: list[str]
     flask_blueprints: list[str]
     fastapi_apps: list[FastAPIAppResponse]
+    fastapi_root_middlewares: list[FastAPIRootMiddlewareResponse]
     appbuilder_views: list[AppBuilderViewResponse]
     appbuilder_menu_items: list[AppBuilderMenuItemResponse]
     global_operator_extra_links: list[str]
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-generated.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
index 7f06a982d9a..c01df4277d4 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
@@ -9826,6 +9826,21 @@ components:
       - name
       title: FastAPIAppResponse
       description: Serializer for Plugin FastAPI App responses.
+    FastAPIRootMiddlewareResponse:
+      properties:
+        middleware:
+          type: string
+          title: Middleware
+        name:
+          type: string
+          title: Name
+      additionalProperties: true
+      type: object
+      required:
+      - middleware
+      - name
+      title: FastAPIRootMiddlewareResponse
+      description: Serializer for Plugin FastAPI root middleware responses.
     GridDAGRunwithTIs:
       properties:
         dag_run_id:
@@ -10319,6 +10334,11 @@ components:
             $ref: '#/components/schemas/FastAPIAppResponse'
           type: array
           title: Fastapi Apps
+        fastapi_root_middlewares:
+          items:
+            $ref: '#/components/schemas/FastAPIRootMiddlewareResponse'
+          type: array
+          title: Fastapi Root Middlewares
         appbuilder_views:
           items:
             $ref: '#/components/schemas/AppBuilderViewResponse'
@@ -10358,6 +10378,7 @@ components:
       - macros
       - flask_blueprints
       - fastapi_apps
+      - fastapi_root_middlewares
       - appbuilder_views
       - appbuilder_menu_items
       - global_operator_extra_links
diff --git a/airflow-core/src/airflow/plugins_manager.py 
b/airflow-core/src/airflow/plugins_manager.py
index 98d8ce02363..34d8f100d75 100644
--- a/airflow-core/src/airflow/plugins_manager.py
+++ b/airflow-core/src/airflow/plugins_manager.py
@@ -68,6 +68,7 @@ macros_modules: list[Any] | None = None
 admin_views: list[Any] | None = None
 flask_blueprints: list[Any] | None = None
 fastapi_apps: list[Any] | None = None
+fastapi_root_middlewares: list[Any] | None = None
 menu_links: list[Any] | None = None
 flask_appbuilder_views: list[Any] | None = None
 flask_appbuilder_menu_links: list[Any] | None = None
@@ -88,6 +89,7 @@ PLUGINS_ATTRIBUTES_TO_DUMP = {
     "admin_views",
     "flask_blueprints",
     "fastapi_apps",
+    "fastapi_root_middlewares",
     "menu_links",
     "appbuilder_views",
     "appbuilder_menu_items",
@@ -151,6 +153,7 @@ class AirflowPlugin:
     admin_views: list[Any] = []
     flask_blueprints: list[Any] = []
     fastapi_apps: list[Any] = []
+    fastapi_root_middlewares: list[Any] = []
     menu_links: list[Any] = []
     appbuilder_views: list[Any] = []
     appbuilder_menu_items: list[Any] = []
@@ -406,8 +409,9 @@ def initialize_fastapi_plugins():
     """Collect extension points for the API."""
     global plugins
     global fastapi_apps
+    global fastapi_root_middlewares
 
-    if fastapi_apps:
+    if fastapi_apps is not None and fastapi_root_middlewares is not None:
         return
 
     ensure_plugins_loaded()
@@ -415,12 +419,14 @@ def initialize_fastapi_plugins():
     if plugins is None:
         raise AirflowPluginException("Can't load plugins.")
 
-    log.debug("Initialize FastAPI plugin")
+    log.debug("Initialize FastAPI plugins")
 
     fastapi_apps = []
+    fastapi_root_middlewares = []
 
     for plugin in plugins:
         fastapi_apps.extend(plugin.fastapi_apps)
+        fastapi_root_middlewares.extend(plugin.fastapi_root_middlewares)
 
 
 def initialize_extra_operators_links_plugins():
@@ -586,6 +592,16 @@ def get_plugin_info(attrs_to_dump: Iterable[str] | None = 
None) -> list[dict[str
                         {**d, "app": qualname(d["app"].__class__) if "app" in 
d else None}
                         for d in getattr(plugin, attr)
                     ]
+                elif attr == "fastapi_root_middlewares":
+                    # remove args and kwargs from plugin info to hide 
potentially sensitive info.
+                    info[attr] = [
+                        {
+                            k: (v if k != "middleware" else 
qualname(middleware_dict["middleware"]))
+                            for k, v in middleware_dict.items()
+                            if k not in ("args", "kwargs")
+                        }
+                        for middleware_dict in getattr(plugin, attr)
+                    ]
                 else:
                     info[attr] = getattr(plugin, attr)
             plugins_info.append(info)
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 4718a06f348..3e231b9f4ea 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -3715,6 +3715,24 @@ export const $FastAPIAppResponse = {
   description: "Serializer for Plugin FastAPI App responses.",
 } as const;
 
+export const $FastAPIRootMiddlewareResponse = {
+  properties: {
+    middleware: {
+      type: "string",
+      title: "Middleware",
+    },
+    name: {
+      type: "string",
+      title: "Name",
+    },
+  },
+  additionalProperties: true,
+  type: "object",
+  required: ["middleware", "name"],
+  title: "FastAPIRootMiddlewareResponse",
+  description: "Serializer for Plugin FastAPI root middleware responses.",
+} as const;
+
 export const $GridDAGRunwithTIs = {
   properties: {
     dag_run_id: {
@@ -4468,6 +4486,13 @@ export const $PluginResponse = {
       type: "array",
       title: "Fastapi Apps",
     },
+    fastapi_root_middlewares: {
+      items: {
+        $ref: "#/components/schemas/FastAPIRootMiddlewareResponse",
+      },
+      type: "array",
+      title: "Fastapi Root Middlewares",
+    },
     appbuilder_views: {
       items: {
         $ref: "#/components/schemas/AppBuilderViewResponse",
@@ -4521,6 +4546,7 @@ export const $PluginResponse = {
     "macros",
     "flask_blueprints",
     "fastapi_apps",
+    "fastapi_root_middlewares",
     "appbuilder_views",
     "appbuilder_menu_items",
     "global_operator_extra_links",
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 91118d67f52..f938fc4ac56 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -981,6 +981,15 @@ export type FastAPIAppResponse = {
   [key: string]: unknown | string;
 };
 
+/**
+ * Serializer for Plugin FastAPI root middleware responses.
+ */
+export type FastAPIRootMiddlewareResponse = {
+  middleware: string;
+  name: string;
+  [key: string]: unknown | string;
+};
+
 /**
  * DAG Run model for the Grid UI.
  */
@@ -1160,6 +1169,7 @@ export type PluginResponse = {
   macros: Array<string>;
   flask_blueprints: Array<string>;
   fastapi_apps: Array<FastAPIAppResponse>;
+  fastapi_root_middlewares: Array<FastAPIRootMiddlewareResponse>;
   appbuilder_views: Array<AppBuilderViewResponse>;
   appbuilder_menu_items: Array<AppBuilderMenuItemResponse>;
   global_operator_extra_links: Array<string>;
diff --git a/airflow-core/tests/unit/cli/commands/test_plugins_command.py 
b/airflow-core/tests/unit/cli/commands/test_plugins_command.py
index 1f7278d9091..b1c0e705a67 100644
--- a/airflow-core/tests/unit/cli/commands/test_plugins_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_plugins_command.py
@@ -84,6 +84,12 @@ class TestPluginsCommand:
                         "name": "Name of the App",
                     }
                 ],
+                "fastapi_root_middlewares": [
+                    {
+                        "middleware": 
"unit.plugins.test_plugin.DummyMiddleware",
+                        "name": "Name of the Middleware",
+                    }
+                ],
                 "appbuilder_views": [
                     {
                         "name": "Test View",
diff --git a/airflow-core/tests/unit/plugins/test_plugin.py 
b/airflow-core/tests/unit/plugins/test_plugin.py
index 9671a20f0f9..9c75bc3fbb9 100644
--- a/airflow-core/tests/unit/plugins/test_plugin.py
+++ b/airflow-core/tests/unit/plugins/test_plugin.py
@@ -20,6 +20,7 @@ from __future__ import annotations
 from fastapi import FastAPI
 from flask import Blueprint
 from flask_appbuilder import BaseView as AppBuilderBaseView, expose
+from starlette.middleware.base import BaseHTTPMiddleware
 
 # This is the class you derive to create a plugin
 from airflow.plugins_manager import AirflowPlugin
@@ -89,6 +90,19 @@ app = FastAPI()
 app_with_metadata = {"app": app, "url_prefix": "/some_prefix", "name": "Name 
of the App"}
 
 
+class DummyMiddleware(BaseHTTPMiddleware):
+    async def dispatch(self, request, call_next):
+        return await call_next(request)
+
+
+middleware_with_metadata = {
+    "middleware": DummyMiddleware,
+    "args": [],
+    "kwargs": {},
+    "name": "Name of the Middleware",
+}
+
+
 # Extend an existing class to avoid the need to implement the full interface
 class CustomCronDataIntervalTimetable(CronDataIntervalTimetable):
     pass
@@ -105,6 +119,7 @@ class AirflowTestPlugin(AirflowPlugin):
     macros = [plugin_macro]
     flask_blueprints = [bp]
     fastapi_apps = [app_with_metadata]
+    fastapi_root_middlewares = [middleware_with_metadata]
     appbuilder_views = [v_appbuilder_package]
     appbuilder_menu_items = [appbuilder_mitem, appbuilder_mitem_toplevel]
     global_operator_extra_links = [
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py 
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index 1c332327ec8..c9fe8aaa0ae 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -516,6 +516,18 @@ class FastAPIAppResponse(BaseModel):
     name: Annotated[str, Field(title="Name")]
 
 
+class FastAPIRootMiddlewareResponse(BaseModel):
+    """
+    Serializer for Plugin FastAPI root middleware responses.
+    """
+
+    model_config = ConfigDict(
+        extra="allow",
+    )
+    middleware: Annotated[str, Field(title="Middleware")]
+    name: Annotated[str, Field(title="Name")]
+
+
 class HTTPExceptionResponse(BaseModel):
     """
     HTTPException Model used for error response.
@@ -562,6 +574,9 @@ class PluginResponse(BaseModel):
     macros: Annotated[list[str], Field(title="Macros")]
     flask_blueprints: Annotated[list[str], Field(title="Flask Blueprints")]
     fastapi_apps: Annotated[list[FastAPIAppResponse], Field(title="Fastapi 
Apps")]
+    fastapi_root_middlewares: Annotated[
+        list[FastAPIRootMiddlewareResponse], Field(title="Fastapi Root 
Middlewares")
+    ]
     appbuilder_views: Annotated[list[AppBuilderViewResponse], 
Field(title="Appbuilder Views")]
     appbuilder_menu_items: Annotated[list[AppBuilderMenuItemResponse], 
Field(title="Appbuilder Menu Items")]
     global_operator_extra_links: Annotated[list[str], Field(title="Global 
Operator Extra Links")]
@@ -1086,6 +1101,8 @@ class DAGDetailsResponse(BaseModel):
     is_active: Annotated[bool, Field(title="Is Active")]
     last_parsed_time: Annotated[datetime | None, Field(title="Last Parsed 
Time")] = None
     last_expired: Annotated[datetime | None, Field(title="Last Expired")] = 
None
+    bundle_name: Annotated[str, Field(title="Bundle Name")]
+    relative_fileloc: Annotated[str, Field(title="Relative Fileloc")]
     fileloc: Annotated[str, Field(title="Fileloc")]
     description: Annotated[str | None, Field(title="Description")] = None
     timetable_summary: Annotated[str | None, Field(title="Timetable Summary")] 
= None
@@ -1137,6 +1154,8 @@ class DAGResponse(BaseModel):
     is_active: Annotated[bool, Field(title="Is Active")]
     last_parsed_time: Annotated[datetime | None, Field(title="Last Parsed 
Time")] = None
     last_expired: Annotated[datetime | None, Field(title="Last Expired")] = 
None
+    bundle_name: Annotated[str, Field(title="Bundle Name")]
+    relative_fileloc: Annotated[str, Field(title="Relative Fileloc")]
     fileloc: Annotated[str, Field(title="Fileloc")]
     description: Annotated[str | None, Field(title="Description")] = None
     timetable_summary: Annotated[str | None, Field(title="Timetable Summary")] 
= None
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py 
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index c372bd6bbf2..c8f50749c02 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -354,6 +354,7 @@ class TestDagOperations:
         last_parsed_time=datetime.datetime(2024, 12, 31, 23, 59, 59),
         last_expired=datetime.datetime(2025, 1, 1, 0, 0, 0),
         fileloc="fileloc",
+        relative_fileloc="relative_fileloc",
         description="description",
         timetable_summary="timetable_summary",
         timetable_description="timetable_description",
@@ -369,6 +370,7 @@ class TestDagOperations:
         next_dagrun_run_after=datetime.datetime(2025, 1, 1, 0, 0, 0),
         owners=["apache-airflow"],
         file_token="file_token",
+        bundle_name="bundle_name",
     )
 
     dag_details_response = DAGDetailsResponse(
@@ -379,6 +381,7 @@ class TestDagOperations:
         last_parsed_time=datetime.datetime(2024, 12, 31, 23, 59, 59),
         last_expired=datetime.datetime(2025, 1, 1, 0, 0, 0),
         fileloc="fileloc",
+        relative_fileloc="relative_fileloc",
         description="description",
         timetable_summary="timetable_summary",
         timetable_description="timetable_description",
@@ -407,6 +410,7 @@ class TestDagOperations:
         last_parsed=datetime.datetime(2024, 12, 31, 23, 59, 59),
         file_token="file_token",
         concurrency=1,
+        bundle_name="bundle_name",
     )
 
     def test_get(self):
diff --git a/devel-common/src/tests_common/test_utils/mock_plugins.py 
b/devel-common/src/tests_common/test_utils/mock_plugins.py
index 0a5e199fcc0..b522159e5a0 100644
--- a/devel-common/src/tests_common/test_utils/mock_plugins.py
+++ b/devel-common/src/tests_common/test_utils/mock_plugins.py
@@ -27,6 +27,7 @@ PLUGINS_MANAGER_NULLABLE_ATTRIBUTES = [
     "admin_views",
     "flask_blueprints",
     "fastapi_apps",
+    "fastapi_root_middlewares",
     "menu_links",
     "flask_appbuilder_views",
     "flask_appbuilder_menu_links",

Reply via email to