This is an automated email from the ASF dual-hosted git repository.
exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 3b3e74d46b NIFI-13824 Installed Python Processor Dependencies with one
command
3b3e74d46b is described below
commit 3b3e74d46bc81482e7ef40a1088852e3901894c8
Author: Ferenc Gerlits <[email protected]>
AuthorDate: Mon Oct 21 17:22:01 2024 +0200
NIFI-13824 Installed Python Processor Dependencies with one command
If a Python processor defines dependencies both inline and in a
requirements.txt file, then we need to install the two groups of
dependencies in a single `pip install` command, otherwise pip is
not able to resolve the web of dependencies correctly.
- Added setup-python step with Python 3.12 to ci-workflow for consistent
version behavior
This closes #9429
Signed-off-by: David Handermann <[email protected]>
---
.github/workflows/ci-workflow.yml | 8 +++
.../src/main/python/framework/ExtensionManager.py | 32 +++++-------
.../test/python/framework/TestExtensionManager.py | 49 ++++++++++++++++++
.../python/framework/TestPythonProcessorAdapter.py | 12 ++---
.../src/test/python/framework/testutils.py | 58 ++++++++++++++++++++++
.../framework/dummy_processor}/DummyProcessor.py | 0
.../ProcessorWithDependencies.py | 33 ++++++++++++
.../processor_with_dependencies/requirements.txt | 16 ++++++
8 files changed, 180 insertions(+), 28 deletions(-)
diff --git a/.github/workflows/ci-workflow.yml
b/.github/workflows/ci-workflow.yml
index cd82a9fc54..44e897e61f 100644
--- a/.github/workflows/ci-workflow.yml
+++ b/.github/workflows/ci-workflow.yml
@@ -162,6 +162,10 @@ jobs:
distribution: 'corretto'
java-version: '21'
cache: 'maven'
+ - name: Set up Python 3.12
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
- name: Evaluate Changed Paths
uses: dorny/paths-filter@v3
id: changes
@@ -227,6 +231,10 @@ jobs:
distribution: 'zulu'
java-version: '21'
cache: 'maven'
+ - name: Set up Python 3.12
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
- name: Evaluate Changed Paths
uses: dorny/paths-filter@v3
id: changes
diff --git
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/main/python/framework/ExtensionManager.py
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/main/python/framework/ExtensionManager.py
index 5e1cad272d..cf27d237c1 100644
---
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/main/python/framework/ExtensionManager.py
+++
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/main/python/framework/ExtensionManager.py
@@ -285,37 +285,31 @@ class ExtensionManager:
logger.info("All dependencies have already been imported for
{0}".format(class_name))
return True
- python_cmd = os.getenv("PYTHON_CMD")
+ dependency_references = []
+
if processor_details.source_location is not None:
package_dir = os.path.dirname(processor_details.source_location)
requirements_file = os.path.join(package_dir, 'requirements.txt')
if os.path.exists(requirements_file):
- args = [python_cmd, '-m', 'pip', 'install', '--no-cache-dir',
'--target', target_dir, '-r', requirements_file]
-
- logger.info(f"Importing dependencies from requirements file
for package {package_dir} to {target_dir} using command {args}")
- result = subprocess.run(args)
+ dependency_references.append('-r')
+ dependency_references.append(requirements_file)
- if result.returncode == 0:
- logger.info(f"Successfully imported requirements for
package {package_dir} to {target_dir}")
- else:
- raise RuntimeError(f"Failed to import requirements for
package {package_dir} from requirements.txt file: process exited with status
code {result}")
+ inline_dependencies = processor_details.getDependencies()
+ for dependency in inline_dependencies:
+ dependency_references.append(dependency)
- dependencies = processor_details.getDependencies()
- if len(dependencies) > 0:
+ if len(dependency_references) > 0:
python_cmd = os.getenv("PYTHON_CMD")
- args = [python_cmd, '-m', 'pip', 'install', '--no-cache-dir',
'--target', target_dir]
- for dep in dependencies:
- args.append(dep)
-
- logger.info(f"Importing dependencies {dependencies} for
{class_name} to {target_dir} using command {args}")
+ args = [python_cmd, '-m', 'pip', 'install', '--no-cache-dir',
'--target', target_dir] + dependency_references
+ logger.info(f"Installing dependencies {dependency_references} for
{class_name} to {target_dir} using command {args}")
result = subprocess.run(args)
if result.returncode == 0:
- logger.info(f"Successfully imported requirements for
{class_name} to {target_dir}")
+ logger.info(f"Successfully installed requirements for
{class_name} to {target_dir}")
else:
- raise RuntimeError(f"Failed to import requirements for
{class_name}: process exited with status code {result}")
+ raise RuntimeError(f"Failed to install requirements for
{class_name}: process exited with status code {result}")
else:
- logger.info(f"No dependencies to import for {class_name}")
+ logger.info(f"No dependencies to install for {class_name}")
# Write a completion Marker File
with open(completion_marker_file, "w") as file:
diff --git
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/TestExtensionManager.py
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/TestExtensionManager.py
new file mode 100644
index 0000000000..5737e23bce
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/TestExtensionManager.py
@@ -0,0 +1,49 @@
+# 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.
+
+import os
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from ExtensionManager import ExtensionManager
+from testutils import set_up_env, get_processor_details
+
+PROCESSOR_WITH_DEPENDENCIES_TEST_FILE =
'src/test/resources/python/framework/processor_with_dependencies/ProcessorWithDependencies.py'
+
+class ReturncodeMocker:
+ def __init__(self, return_code):
+ self.returncode = return_code
+
+class TestExtensionManager(unittest.TestCase):
+ def setUp(self):
+ set_up_env()
+ self.extension_manager = ExtensionManager(None)
+
+ @patch('subprocess.run')
+ def test_import_external_dependencies(self, mock_subprocess_run):
+ details = get_processor_details(self, 'ProcessorWithDependencies',
PROCESSOR_WITH_DEPENDENCIES_TEST_FILE,
'/extensions/processor_with_dependencies')
+ self.assertIsNotNone(details)
+
+ mock_subprocess_run.return_value = ReturncodeMocker(0)
+
+ with tempfile.TemporaryDirectory() as temp_dir:
+ packages_dir = os.path.join(temp_dir, 'packages')
+ self.extension_manager.import_external_dependencies(details,
packages_dir)
+
+ mock_subprocess_run.assert_called_once()
+
+if __name__ == '__main__':
+ unittest.main()
diff --git
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/TestPythonProcessorAdapter.py
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/TestPythonProcessorAdapter.py
index b8b9b9038b..95eaa147c2 100644
---
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/TestPythonProcessorAdapter.py
+++
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/TestPythonProcessorAdapter.py
@@ -13,20 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import ProcessorInspection
import unittest
+from testutils import get_processor_details
-DUMMY_PROCESSOR_FILE = 'src/test/python/framework/DummyProcessor.py'
+DUMMY_PROCESSOR_FILE =
'src/test/resources/python/framework/dummy_processor/DummyProcessor.py'
class DetectProcessorUseCase(unittest.TestCase):
def test_get_processor_details(self):
- class_nodes =
ProcessorInspection.get_processor_class_nodes(DUMMY_PROCESSOR_FILE)
- self.assertIsNotNone(class_nodes)
- self.assertEqual(len(class_nodes), 1)
- class_node = class_nodes[0]
- self.assertEqual(class_node.name, 'DummyProcessor')
-
- details = ProcessorInspection.get_processor_details(class_node,
DUMMY_PROCESSOR_FILE, '/extensions/dummy_processor', False)
+ details = get_processor_details(self, 'DummyProcessor',
DUMMY_PROCESSOR_FILE, '/extensions/dummy_processor')
self.assertIsNotNone(details)
self.assertEqual(details.description, 'Fake Processor')
self.assertEqual(details.tags, ['tag1', 'tag2'])
diff --git
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/testutils.py
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/testutils.py
new file mode 100644
index 0000000000..bdee92ea48
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/testutils.py
@@ -0,0 +1,58 @@
+# 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.
+
+import os
+import sys
+from nifiapi.__jvm__ import JvmHolder
+import ProcessorInspection
+
+class FakeJvm:
+ def __init__(self):
+ self.java = FakeJava()
+
+class FakeJava:
+ def __init__(self):
+ self.util = FakeJavaUtil()
+
+class FakeJavaUtil:
+ def ArrayList(self):
+ return FakeArrayList([])
+
+class FakeArrayList:
+ def __init__(self, my_list):
+ self.my_list = my_list
+
+ def __len__(self):
+ return len(self.my_list)
+
+ def __iter__(self):
+ return iter(self.my_list)
+
+ def add(self, element):
+ self.my_list.append(element)
+
+def set_up_env():
+ python_command = sys.executable
+ os.environ["PYTHON_CMD"] = python_command
+ JvmHolder.jvm = FakeJvm()
+
+def get_processor_details(test_fixture, processor_name, processor_file,
extension_home):
+ class_nodes = ProcessorInspection.get_processor_class_nodes(processor_file)
+ test_fixture.assertIsNotNone(class_nodes)
+ test_fixture.assertEqual(len(class_nodes), 1)
+ class_node = class_nodes[0]
+ test_fixture.assertEqual(class_node.name, processor_name)
+
+ return ProcessorInspection.get_processor_details(class_node,
processor_file, extension_home, False)
diff --git
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/DummyProcessor.py
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/resources/python/framework/dummy_processor/DummyProcessor.py
similarity index 100%
rename from
nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/python/framework/DummyProcessor.py
rename to
nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/resources/python/framework/dummy_processor/DummyProcessor.py
diff --git
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/resources/python/framework/processor_with_dependencies/ProcessorWithDependencies.py
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/resources/python/framework/processor_with_dependencies/ProcessorWithDependencies.py
new file mode 100644
index 0000000000..ec0e4e6ef3
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/resources/python/framework/processor_with_dependencies/ProcessorWithDependencies.py
@@ -0,0 +1,33 @@
+# 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 nifiapi.flowfiletransform import FlowFileTransform,
FlowFileTransformResult
+
+class ProcessorWithDependencies(FlowFileTransform):
+ class Java:
+ implements = ['org.apache.nifi.python.processor.FlowFileTransform']
+
+ class ProcessorDetails:
+ description = "This processor depends on both google-cloud-vision and
pymilvus"
+ version = '0.0.1'
+ tags = ['cloud', 'vision', 'milvus']
+ dependencies = ['pymilvus==2.4.4']
+
+ def __init__(self):
+ pass
+
+ def transform(self, context, flow_file):
+ self.logger.info("ProcessorWithDependencies is returning")
+ return FlowFileTransformResult('success', contents='foobar')
diff --git
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/resources/python/framework/processor_with_dependencies/requirements.txt
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/resources/python/framework/processor_with_dependencies/requirements.txt
new file mode 100644
index 0000000000..2221cbe503
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-framework/src/test/resources/python/framework/processor_with_dependencies/requirements.txt
@@ -0,0 +1,16 @@
+# 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.
+
+google-cloud-vision==3.7.4