This is an automated email from the ASF dual-hosted git repository.
rnhttr 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 fa10a93338b OpenLineage EmptyOperator (#53730)
fa10a93338b is described below
commit fa10a93338bc7e15d1ad464578e5d22fa43cf9e8
Author: Ryan Hatter <[email protected]>
AuthorDate: Sat Jul 26 16:11:23 2025 -0400
OpenLineage EmptyOperator (#53730)
* add EmptyOperator with lineage
* add EmptyOperator with lineage
* update provider.yaml
---
providers/openlineage/provider.yaml | 5 ++
.../providers/openlineage/get_provider_info.py | 6 +++
.../{version_compat.py => operators/__init__.py} | 19 --------
.../providers/openlineage/operators/empty.py | 48 +++++++++++++++++++
.../providers/openlineage/version_compat.py | 10 ++++
.../unit/openlineage/operators/__init__.py} | 19 --------
.../tests/unit/openlineage/operators/test_empty.py | 54 ++++++++++++++++++++++
7 files changed, 123 insertions(+), 38 deletions(-)
diff --git a/providers/openlineage/provider.yaml
b/providers/openlineage/provider.yaml
index be450c26905..bf77c0bc824 100644
--- a/providers/openlineage/provider.yaml
+++ b/providers/openlineage/provider.yaml
@@ -68,6 +68,11 @@ integrations:
logo: /docs/integration-logos/openlineage.svg
tags: [protocol]
+operators:
+ - integration-name: OpenLineage
+ python-modules:
+ - airflow.providers.openlineage.operators.empty
+
plugins:
- name: openlineage
plugin-class:
airflow.providers.openlineage.plugins.openlineage.OpenLineageProviderPlugin
diff --git
a/providers/openlineage/src/airflow/providers/openlineage/get_provider_info.py
b/providers/openlineage/src/airflow/providers/openlineage/get_provider_info.py
index dd59cfacca4..3a26a47bdfd 100644
---
a/providers/openlineage/src/airflow/providers/openlineage/get_provider_info.py
+++
b/providers/openlineage/src/airflow/providers/openlineage/get_provider_info.py
@@ -34,6 +34,12 @@ def get_provider_info():
"tags": ["protocol"],
}
],
+ "operators": [
+ {
+ "integration-name": "OpenLineage",
+ "python-modules":
["airflow.providers.openlineage.operators.empty"],
+ }
+ ],
"plugins": [
{
"name": "openlineage",
diff --git
a/providers/openlineage/src/airflow/providers/openlineage/version_compat.py
b/providers/openlineage/src/airflow/providers/openlineage/operators/__init__.py
similarity index 51%
copy from
providers/openlineage/src/airflow/providers/openlineage/version_compat.py
copy to
providers/openlineage/src/airflow/providers/openlineage/operators/__init__.py
index 48d122b6696..13a83393a91 100644
--- a/providers/openlineage/src/airflow/providers/openlineage/version_compat.py
+++
b/providers/openlineage/src/airflow/providers/openlineage/operators/__init__.py
@@ -14,22 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-#
-# NOTE! THIS FILE IS COPIED MANUALLY IN OTHER PROVIDERS DELIBERATELY TO AVOID
ADDING UNNECESSARY
-# DEPENDENCIES BETWEEN PROVIDERS. IF YOU WANT TO ADD CONDITIONAL CODE IN YOUR
PROVIDER THAT DEPENDS
-# ON AIRFLOW VERSION, PLEASE COPY THIS FILE TO THE ROOT PACKAGE OF YOUR
PROVIDER AND IMPORT
-# THOSE CONSTANTS FROM IT RATHER THAN IMPORTING THEM FROM ANOTHER PROVIDER OR
TEST CODE
-#
-from __future__ import annotations
-
-
-def get_base_airflow_version_tuple() -> tuple[int, int, int]:
- from packaging.version import Version
-
- from airflow import __version__
-
- airflow_version = Version(__version__)
- return airflow_version.major, airflow_version.minor, airflow_version.micro
-
-
-AIRFLOW_V_3_0_PLUS = get_base_airflow_version_tuple() >= (3, 0, 0)
diff --git
a/providers/openlineage/src/airflow/providers/openlineage/operators/empty.py
b/providers/openlineage/src/airflow/providers/openlineage/operators/empty.py
new file mode 100644
index 00000000000..6ac8754797f
--- /dev/null
+++ b/providers/openlineage/src/airflow/providers/openlineage/operators/empty.py
@@ -0,0 +1,48 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from airflow.providers.openlineage.extractors.base import OperatorLineage
+from airflow.providers.openlineage.version_compat import BaseOperator
+
+if TYPE_CHECKING:
+ from airflow.sdk.definitions.context import Context
+
+
+class EmptyOperator(BaseOperator):
+ """
+ Operator that does literally nothing.
+
+ It can be used to group tasks in a DAG.
+ The task is evaluated by the scheduler but never processed by the executor.
+ """
+
+ ui_color = "#e8f7e4"
+
+ def execute(self, context: Context):
+ pass
+
+ def get_openlineage_facets_on_start(self) -> OperatorLineage:
+ return OperatorLineage()
+
+ def get_openlineage_facets_on_complete(self, task_instance) ->
OperatorLineage:
+ return OperatorLineage()
+
+ def get_openlineage_facets_on_failure(self, task_instance) ->
OperatorLineage:
+ return OperatorLineage()
diff --git
a/providers/openlineage/src/airflow/providers/openlineage/version_compat.py
b/providers/openlineage/src/airflow/providers/openlineage/version_compat.py
index 48d122b6696..e7a259afb35 100644
--- a/providers/openlineage/src/airflow/providers/openlineage/version_compat.py
+++ b/providers/openlineage/src/airflow/providers/openlineage/version_compat.py
@@ -33,3 +33,13 @@ def get_base_airflow_version_tuple() -> tuple[int, int, int]:
AIRFLOW_V_3_0_PLUS = get_base_airflow_version_tuple() >= (3, 0, 0)
+
+if AIRFLOW_V_3_0_PLUS:
+ from airflow.sdk import BaseOperator
+else:
+ from airflow.models import BaseOperator
+
+__all__ = [
+ "AIRFLOW_V_3_0_PLUS",
+ "BaseOperator",
+]
diff --git
a/providers/openlineage/src/airflow/providers/openlineage/version_compat.py
b/providers/openlineage/tests/unit/openlineage/operators/__init__.py
similarity index 51%
copy from
providers/openlineage/src/airflow/providers/openlineage/version_compat.py
copy to providers/openlineage/tests/unit/openlineage/operators/__init__.py
index 48d122b6696..13a83393a91 100644
--- a/providers/openlineage/src/airflow/providers/openlineage/version_compat.py
+++ b/providers/openlineage/tests/unit/openlineage/operators/__init__.py
@@ -14,22 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-#
-# NOTE! THIS FILE IS COPIED MANUALLY IN OTHER PROVIDERS DELIBERATELY TO AVOID
ADDING UNNECESSARY
-# DEPENDENCIES BETWEEN PROVIDERS. IF YOU WANT TO ADD CONDITIONAL CODE IN YOUR
PROVIDER THAT DEPENDS
-# ON AIRFLOW VERSION, PLEASE COPY THIS FILE TO THE ROOT PACKAGE OF YOUR
PROVIDER AND IMPORT
-# THOSE CONSTANTS FROM IT RATHER THAN IMPORTING THEM FROM ANOTHER PROVIDER OR
TEST CODE
-#
-from __future__ import annotations
-
-
-def get_base_airflow_version_tuple() -> tuple[int, int, int]:
- from packaging.version import Version
-
- from airflow import __version__
-
- airflow_version = Version(__version__)
- return airflow_version.major, airflow_version.minor, airflow_version.micro
-
-
-AIRFLOW_V_3_0_PLUS = get_base_airflow_version_tuple() >= (3, 0, 0)
diff --git
a/providers/openlineage/tests/unit/openlineage/operators/test_empty.py
b/providers/openlineage/tests/unit/openlineage/operators/test_empty.py
new file mode 100644
index 00000000000..ab7427d63ad
--- /dev/null
+++ b/providers/openlineage/tests/unit/openlineage/operators/test_empty.py
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from unittest.mock import Mock
+
+import pytest
+
+from airflow.providers.openlineage.extractors.base import OperatorLineage
+from airflow.providers.openlineage.operators.empty import EmptyOperator
+
+
+def test_execute_returns_none_and_does_not_raise():
+ op = EmptyOperator(task_id="empty")
+ # Calling execute should do nothing and return None
+ result = op.execute(Mock())
+ assert result is None
+
+
[email protected](
+ "method_name,use_task_instance",
+ [
+ ("get_openlineage_facets_on_start", False),
+ ("get_openlineage_facets_on_complete", True),
+ ("get_openlineage_facets_on_failure", True),
+ ],
+)
+def test_openlineage_facets_methods_return_operator_lineage(method_name,
use_task_instance):
+ op = EmptyOperator(task_id="empty")
+ method = getattr(op, method_name)
+ # Invoke with or without a mock for task_instance
+ if use_task_instance:
+ facets = method(Mock())
+ else:
+ facets = method()
+ # Should return an OperatorLineage instance
+ assert isinstance(facets, OperatorLineage)
+ # Each call returns a fresh instance
+ second_call = method(Mock()) if use_task_instance else method()
+ assert facets is not second_call