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 2acc1038c9 NIFI-12616 Added Processor Documentation Support for Python
2acc1038c9 is described below
commit 2acc1038c988f02487a13679e403f492db45ff47
Author: Mark Payne <[email protected]>
AuthorDate: Wed Jan 3 15:06:03 2024 -0500
NIFI-12616 Added Processor Documentation Support for Python
- Added some Use Case docs for Python processors and updated Runtime
Manifests to include Python based processors as well as Use
Case/MultiProcessorUseCase documentation elements. Refactored/cleaned up some
of the Python code and added unit tests.
- Added python-unit-tests profile and enabled on Ubuntu and macOS GitHub
workflows
This closes #8253
Signed-off-by: David Handermann <[email protected]>
---
.github/workflows/ci-workflow.yml | 2 +
.gitignore | 1 +
.../component/api/MultiProcessorUseCase.java | 67 +++++
.../component/api/ProcessorConfiguration.java | 46 ++++
.../component/api/ProcessorDefinition.java | 24 +-
.../nifi/c2/protocol/component/api/UseCase.java | 77 ++++++
.../src/main/asciidoc/python-developer-guide.adoc | 82 ++++++
.../impl/StandardRuntimeManifestBuilder.java | 119 +++++----
.../components/ClassLoaderAwarePythonBridge.java | 4 +-
.../apache/nifi/controller/ExtensionBuilder.java | 2 +-
.../manifest/StandardRuntimeManifestService.java | 205 +++++++++++++-
.../org/apache/nifi/nar/ExtensionDefinition.java | 2 +-
.../java/org/apache/nifi/nar/ExtensionManager.java | 9 +
.../nar/StandardExtensionDiscoveringManager.java | 38 ++-
.../java/org/apache/nifi/nar/PythonBundle.java | 7 +-
.../org/apache/nifi/py4j/StandardPythonBridge.java | 9 +-
.../nifi/py4j/StandardPythonProcessorBridge.java | 2 +-
.../python/processor/FlowFileTransformProxy.java | 4 +-
.../python/processor/PythonProcessorProxy.java | 12 +-
.../python/processor/RecordTransformProxy.java | 4 +-
.../PythonControllerInteractionIT.java | 2 +-
.../src/main/python/src/nifiapi/__jvm__.py | 8 +-
.../src/main/python/src/nifiapi/documentation.py | 142 ++++++++++
.../apache/nifi/python/DisabledPythonBridge.java | 2 +-
.../java/org/apache/nifi/python/PythonBridge.java | 3 +-
.../apache/nifi/python/PythonProcessorDetails.java | 22 ++
.../MultiProcessorUseCaseDetails.java | 30 +++
.../ProcessorConfigurationDetails.java | 24 ++
.../documentation/PropertyDescription.java | 36 +++
.../processor/documentation/UseCaseDetails.java | 30 +++
.../nifi-py4j-bundle/nifi-python-framework/pom.xml | 42 +++
.../src/main/python/framework/ExtensionDetails.py | 74 ++++++
.../src/main/python/framework/ExtensionManager.py | 260 ++----------------
.../main/python/framework/ProcessorInspection.py | 295 +++++++++++++++++++++
.../python/framework/PythonProcessorAdapter.py | 43 +--
.../src/test/python/framework/DummyProcessor.py | 45 ++++
.../python/framework/TestPythonProcessorAdapter.py | 53 ++++
.../src/main/python/PromptChatGPT.py | 2 +-
.../src/main/python/ChunkDocument.py | 78 +++++-
.../src/main/python/vectorstores/PutPinecone.py | 31 ++-
40 files changed, 1591 insertions(+), 347 deletions(-)
diff --git a/.github/workflows/ci-workflow.yml
b/.github/workflows/ci-workflow.yml
index 1c3b97fef0..9aacfdf0a8 100644
--- a/.github/workflows/ci-workflow.yml
+++ b/.github/workflows/ci-workflow.yml
@@ -170,6 +170,7 @@ jobs:
${{ env.MAVEN_VERIFY_COMMAND }}
${{ env.MAVEN_BUILD_PROFILES }}
-P report-code-coverage
+ -P python-unit-tests
${{ env.MAVEN_PROJECTS }}
- name: Codecov
uses: codecov/codecov-action@v3
@@ -238,6 +239,7 @@ jobs:
${{ env.MAVEN_COMMAND }}
${{ env.MAVEN_VERIFY_COMMAND }}
${{ env.MAVEN_BUILD_PROFILES }}
+ -P python-unit-tests
${{ env.MAVEN_PROJECTS }}
- name: Upload Test Reports
uses: actions/upload-artifact@v3
diff --git a/.gitignore b/.gitignore
index 2e9685e07b..f45e08f9fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ nb-configuration.xml
.vscode/
.java-version
/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-extension-api/src/main/python/dist/
+__pycache__
diff --git
a/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/MultiProcessorUseCase.java
b/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/MultiProcessorUseCase.java
new file mode 100644
index 0000000000..ccc238bdee
--- /dev/null
+++
b/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/MultiProcessorUseCase.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+package org.apache.nifi.c2.protocol.component.api;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.io.Serializable;
+import java.util.List;
+
+public class MultiProcessorUseCase implements Serializable {
+ private String description;
+ private String notes;
+ private List<String> keywords;
+ private List<ProcessorConfiguration> configurations;
+
+ @Schema(description="A description of the use case")
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ @Schema(description="Any pertinent notes about the use case")
+ public String getNotes() {
+ return notes;
+ }
+
+ public void setNotes(final String notes) {
+ this.notes = notes;
+ }
+
+ @Schema(description="Keywords that pertain to the use csae")
+ public List<String> getKeywords() {
+ return keywords;
+ }
+
+ public void setKeywords(final List<String> keywords) {
+ this.keywords = keywords;
+ }
+
+ @Schema(description="A description of how to configure the Processor to
perform the task described in the use case")
+ public List<ProcessorConfiguration> getConfigurations() {
+ return configurations;
+ }
+
+ public void setConfigurations(final List<ProcessorConfiguration>
configurations) {
+ this.configurations = configurations;
+ }
+
+}
diff --git
a/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/ProcessorConfiguration.java
b/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/ProcessorConfiguration.java
new file mode 100644
index 0000000000..0416e19e47
--- /dev/null
+++
b/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/ProcessorConfiguration.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+
+package org.apache.nifi.c2.protocol.component.api;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.io.Serializable;
+
+public class ProcessorConfiguration implements Serializable {
+ private String processorClassName;
+ private String configuration;
+
+ @Schema(description="The fully qualified classname of the Processor that
should be used to accomplish the use case")
+ public String getProcessorClassName() {
+ return processorClassName;
+ }
+
+ public void setProcessorClassName(final String processorClassName) {
+ this.processorClassName = processorClassName;
+ }
+
+ @Schema(description="A description of how the Processor should be
configured in order to accomplish the use case")
+ public String getConfiguration() {
+ return configuration;
+ }
+
+ public void setConfiguration(final String configuration) {
+ this.configuration = configuration;
+ }
+}
diff --git
a/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/ProcessorDefinition.java
b/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/ProcessorDefinition.java
index ef092d54a2..f35a6add63 100644
---
a/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/ProcessorDefinition.java
+++
b/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/ProcessorDefinition.java
@@ -51,7 +51,11 @@ public class ProcessorDefinition extends
ConfigurableExtensionDefinition {
private List<Attribute> readsAttributes;
private List<Attribute> writesAttributes;
- @Schema(description = "Any input requirements this processor has.")
+ private List<UseCase> useCases;
+ private List<MultiProcessorUseCase> multiProcessorUseCases;
+
+
+ @Schema(description="Any input requirements this processor has.")
public InputRequirement.Requirement getInputRequirement() {
return inputRequirement;
}
@@ -225,4 +229,22 @@ public class ProcessorDefinition extends
ConfigurableExtensionDefinition {
public void setWritesAttributes(List<Attribute> writesAttributes) {
this.writesAttributes = writesAttributes;
}
+
+ @Schema(description="A list of use cases that have been documented for
this Processor")
+ public List<UseCase> getUseCases() {
+ return useCases;
+ }
+
+ public void setUseCases(final List<UseCase> useCases) {
+ this.useCases = useCases;
+ }
+
+ @Schema(description="A list of use cases that have been documented that
involve this Processor in conjunction with other Processors")
+ public List<MultiProcessorUseCase> getMultiProcessorUseCases() {
+ return multiProcessorUseCases;
+ }
+
+ public void setMultiProcessorUseCases(final List<MultiProcessorUseCase>
multiProcessorUseCases) {
+ this.multiProcessorUseCases = multiProcessorUseCases;
+ }
}
diff --git
a/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/UseCase.java
b/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/UseCase.java
new file mode 100644
index 0000000000..6f3c7eba74
--- /dev/null
+++
b/c2/c2-protocol/c2-protocol-component-api/src/main/java/org/apache/nifi/c2/protocol/component/api/UseCase.java
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+package org.apache.nifi.c2.protocol.component.api;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+
+import java.io.Serializable;
+import java.util.List;
+
+public class UseCase implements Serializable {
+ private String description;
+ private String notes;
+ private List<String> keywords;
+ private String configuration;
+ private InputRequirement.Requirement inputRequirement;
+
+ @Schema(description="A description of the use case")
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ @Schema(description="Any pertinent notes about the use case")
+ public String getNotes() {
+ return notes;
+ }
+
+ public void setNotes(final String notes) {
+ this.notes = notes;
+ }
+
+ @Schema(description="Keywords that pertain to the use case")
+ public List<String> getKeywords() {
+ return keywords;
+ }
+
+ public void setKeywords(final List<String> keywords) {
+ this.keywords = keywords;
+ }
+
+ @Schema(description="A description of how to configure the Processor to
perform the task described in the use case")
+ public String getConfiguration() {
+ return configuration;
+ }
+
+ public void setConfiguration(final String configuration) {
+ this.configuration = configuration;
+ }
+
+ @Schema(description="Specifies whether an incoming FlowFile is expected
for this use case")
+ public InputRequirement.Requirement getInputRequirement() {
+ return inputRequirement;
+ }
+
+ public void setInputRequirement(final InputRequirement.Requirement
inputRequirement) {
+ this.inputRequirement = inputRequirement;
+ }
+}
diff --git a/nifi-docs/src/main/asciidoc/python-developer-guide.adoc
b/nifi-docs/src/main/asciidoc/python-developer-guide.adoc
index f851aa8364..d610c482a6 100644
--- a/nifi-docs/src/main/asciidoc/python-developer-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/python-developer-guide.adoc
@@ -424,6 +424,88 @@ that there are no longer any invocations of the
`transform` method running when
+[[documenting_use_cases]]
+== Documenting Use Cases
+
+No matter how powerful a piece of software is, it has no value unless people
are able to use it. To that end, documentation of Processors is
+very important. While a description of the Processor should be provided in the
`ProcessorDetails` class and each PropertyDescriptor is expected to have a
description,
+it is usually helpful to also call out specific use cases that can be
performed by the Processor. This is particularly important for Processors that
perform
+more generalized transformations on objects, where a single Processor may be
capable of performing multiple tasks, based on its configuration.
+
+[[use_case_decorator]]
+=== The `@use_case` Decorator
+
+The `@use_case` decorator, defined in the `nifiapi.documentation` module can
facilitate this. The decorator takes four arguments:
+
+- `description`: A simple 1 (at most 2) sentence description of the use case.
Generally, this should not include any extraneous details,
+ such as caveats, etc. Those can be provided using the `notes`
argument. The description is required.
+- `notes`: Most of the time, 1-2 sentences is sufficient to describe a use
case. Those 1-2 sentence should then be returned
+ by the `description`. In the event that the description is not
sufficient, details may be provided to
+ further explain, by providing caveats, etc. This is optional.
+- `keywords`: An array of keywords that can be associated with the use case.
This is optional.
+- `configuration`: A description of how to configure the Processor for this
particular use case. This may include explicit values to set for some
properties,
+ and may include instructions on how to choose the
appropriate value for other properties. The configuration is required.
+
+A single Processor may have multiple `@use_case` decorators.
+
+
+[[multi_processor_use_case_decorator]]
+=== The `@multi_processor_use_case` Decorator
+
+When designing and creating Processors, it is important to keep in mind the
idea of loose coupling. One Processor should not be dependent on another
Processor
+in order to perform its task. That being said, it is often advantageous to
build Processors that are designed to work well together. For example, a
Processor that
+is able to perform a listing of files in a directory can provide an important
capability in and of itself. Similarly, a Processor that is able to ingest the
contents
+of a specific file and make that file's contents the contents of a FlowFile is
also an important capability in and of itself. But far more powerful than
either of these
+individual capabilities is the notion of being able to compose a flow that
lists all files in a directory and then ingests each of those files as a
FlowFile. This is
+done by using a combination of the two. As such, it is important that the two
Processors be able to work together in such a way that the output of the first
is
+easily understood as the input of the second.
+
+In this case, it makes sense to document this composition of Processors as a
use case so that users can understand how to compose such a pipeline. This is
accomplished
+by using the `@multi_processor_use_case` decorator. This decorator is very
similar to the <<use_case_decorator>> but instead of a `configuration` element,
it has a
+`configurations` element, which is a `list` of `ProcessorConfiguration`
objects, where each `ProcessorConfiguration` object has both a
`processor_type`, which is the
+name of the Processor, and a `configuration` that explains how to configure
that particular Processor. The `configuration` element typically also explains
how to connect
+outbound Relationships.
+
+For example, we might use these decorators as such:
+----
+@use_case(description="Retrieve the contents of a given file on disk and
create a FlowFile from it without modifying the file",
+ keywords=["file", "filesystem"],
+ configuration="""
+ Set the 'Filename' property to the fully qualified path of the
file to ingest
+ Set the 'Completion Strategy' to 'None'
+ """)
+@use_case(description="Retrieve the contents of a given file on disk and
create a FlowFile from it, deleting the local file upon success",
+ keywords=["file", "filesystem"],
+ configuration="""
+ Set the 'Filename' property to the fully qualified path of the
file to ingest
+ Set the 'Completion Strategy' to 'Delete'
+ """)
+@multi_processor_use_case(
+ description="Ingest all files from a landing directory on the filesystem
and delete them after ingesting them.",
+ keywords=["file", "filesystem", "landing directory"],
+ configurations=[
+ ProcessorConfiguration(
+ processor_type="org.apache.nifi.processors.standard.ListFile",
+ configuration="""
+ Set 'Input Directory' to the directory that files should be
ingested from
+ Set 'Input Directory Location' to 'Local'
+ """
+ ),
+ ProcessorConfiguration(
+ processor_type="FetchFile",
+ configuration="""
+ Set the 'Filename' property to `${absolute.path}/${filename}`
+ Set the 'Completion Strategy' to 'Delete'
+ """
+ )
+ ])
+class FetchFile(FlowFileTransform):
+----
+
+Note that in this case, we are able to specifically tell the user that the
Filename property of FetchFile should be set to the value
`${absolute.path}/${filename}`
+because we know that the ListFile Processor will produce these attributes for
us.
+
+
[[requirements]]
== Requirements
diff --git
a/nifi-manifest/nifi-runtime-manifest-core/src/main/java/org/apache/nifi/runtime/manifest/impl/StandardRuntimeManifestBuilder.java
b/nifi-manifest/nifi-runtime-manifest-core/src/main/java/org/apache/nifi/runtime/manifest/impl/StandardRuntimeManifestBuilder.java
index e027b13d28..2fa5446f56 100644
---
a/nifi-manifest/nifi-runtime-manifest-core/src/main/java/org/apache/nifi/runtime/manifest/impl/StandardRuntimeManifestBuilder.java
+++
b/nifi-manifest/nifi-runtime-manifest-core/src/main/java/org/apache/nifi/runtime/manifest/impl/StandardRuntimeManifestBuilder.java
@@ -23,6 +23,8 @@ import
org.apache.nifi.c2.protocol.component.api.ConfigurableComponentDefinition
import org.apache.nifi.c2.protocol.component.api.ControllerServiceDefinition;
import org.apache.nifi.c2.protocol.component.api.DefinedType;
import org.apache.nifi.c2.protocol.component.api.ExtensionComponent;
+import org.apache.nifi.c2.protocol.component.api.MultiProcessorUseCase;
+import org.apache.nifi.c2.protocol.component.api.ProcessorConfiguration;
import org.apache.nifi.c2.protocol.component.api.ProcessorDefinition;
import org.apache.nifi.c2.protocol.component.api.PropertyAllowableValue;
import org.apache.nifi.c2.protocol.component.api.PropertyDependency;
@@ -33,6 +35,7 @@ import
org.apache.nifi.c2.protocol.component.api.ReportingTaskDefinition;
import org.apache.nifi.c2.protocol.component.api.Restriction;
import org.apache.nifi.c2.protocol.component.api.RuntimeManifest;
import org.apache.nifi.c2.protocol.component.api.SchedulingDefaults;
+import org.apache.nifi.c2.protocol.component.api.UseCase;
import org.apache.nifi.components.resource.ResourceCardinality;
import org.apache.nifi.components.resource.ResourceType;
import org.apache.nifi.components.state.Scope;
@@ -151,7 +154,7 @@ public class StandardRuntimeManifestBuilder implements
RuntimeManifestBuilder {
@Override
public RuntimeManifestBuilder addBundles(final
Iterable<ExtensionManifestContainer> extensionManifests) {
- extensionManifests.forEach(em -> addBundle(em));
+ extensionManifests.forEach(this::addBundle);
return this;
}
@@ -277,9 +280,45 @@ public class StandardRuntimeManifestBuilder implements
RuntimeManifestBuilder {
);
}
+ final List<UseCase> useCases = extension.getUseCases() == null ?
List.of() : extension.getUseCases().stream()
+ .map(StandardRuntimeManifestBuilder::createUseCase)
+ .toList();
+ processorDefinition.setUseCases(useCases);
+
+ final List<MultiProcessorUseCase> multiProcessorUseCases =
extension.getMultiProcessorUseCases() == null ? List.of() :
extension.getMultiProcessorUseCases().stream()
+ .map(StandardRuntimeManifestBuilder::createMultiProcessorUseCase)
+ .toList();
+ processorDefinition.setMultiProcessorUseCases(multiProcessorUseCases);
+
componentManifestBuilder.addProcessor(processorDefinition);
}
+ private static UseCase createUseCase(final
org.apache.nifi.extension.manifest.UseCase extensionUseCase) {
+ final UseCase useCase = new UseCase();
+ useCase.setDescription(extensionUseCase.getDescription());
+ useCase.setConfiguration(extensionUseCase.getConfiguration());
+ useCase.setKeywords(extensionUseCase.getKeywords());
+ useCase.setNotes(extensionUseCase.getNotes());
+ return useCase;
+ }
+
+ private static MultiProcessorUseCase createMultiProcessorUseCase(final
org.apache.nifi.extension.manifest.MultiProcessorUseCase extensionUseCase) {
+ final MultiProcessorUseCase useCase = new MultiProcessorUseCase();
+ useCase.setDescription(extensionUseCase.getDescription());
+ useCase.setKeywords(extensionUseCase.getKeywords());
+ useCase.setNotes(extensionUseCase.getNotes());
+
+ final List<ProcessorConfiguration> processorConfigs = new
ArrayList<>();
+ for (final org.apache.nifi.extension.manifest.ProcessorConfiguration
extensionConfig : extensionUseCase.getProcessorConfigurations()) {
+ final ProcessorConfiguration processorConfig = new
ProcessorConfiguration();
+
processorConfig.setConfiguration(extensionConfig.getConfiguration());
+
processorConfig.setProcessorClassName(extensionConfig.getProcessorClassName());
+ processorConfigs.add(processorConfig);
+ }
+ useCase.setConfigurations(processorConfigs);
+ return useCase;
+ }
+
private org.apache.nifi.c2.protocol.component.api.Attribute
getAttribute(final Attribute attribute) {
final org.apache.nifi.c2.protocol.component.api.Attribute c2Attribute
= new org.apache.nifi.c2.protocol.component.api.Attribute();
c2Attribute.setName(attribute.getName());
@@ -299,16 +338,11 @@ public class StandardRuntimeManifestBuilder implements
RuntimeManifestBuilder {
return null;
}
- switch (inputRequirement) {
- case INPUT_ALLOWED:
- return InputRequirement.Requirement.INPUT_ALLOWED;
- case INPUT_REQUIRED:
- return InputRequirement.Requirement.INPUT_REQUIRED;
- case INPUT_FORBIDDEN:
- return InputRequirement.Requirement.INPUT_FORBIDDEN;
- default:
- throw new IllegalArgumentException("Unknown input requirement:
" + inputRequirement.name());
- }
+ return switch (inputRequirement) {
+ case INPUT_ALLOWED -> InputRequirement.Requirement.INPUT_ALLOWED;
+ case INPUT_REQUIRED -> InputRequirement.Requirement.INPUT_REQUIRED;
+ case INPUT_FORBIDDEN ->
InputRequirement.Requirement.INPUT_FORBIDDEN;
+ };
}
private List<Relationship> getSupportedRelationships(final
List<org.apache.nifi.extension.manifest.Relationship> relationships) {
@@ -460,14 +494,10 @@ public class StandardRuntimeManifestBuilder implements
RuntimeManifestBuilder {
}
private Scope getScope(final org.apache.nifi.extension.manifest.Scope
sourceScope) {
- switch (sourceScope) {
- case LOCAL:
- return Scope.LOCAL;
- case CLUSTER:
- return Scope.CLUSTER;
- default:
- throw new IllegalArgumentException("Unknown scope: " +
sourceScope);
- }
+ return switch (sourceScope) {
+ case LOCAL -> Scope.LOCAL;
+ case CLUSTER -> Scope.CLUSTER;
+ };
}
private Restriction createRestriction(final
org.apache.nifi.extension.manifest.Restriction extensionRestriction) {
@@ -550,8 +580,7 @@ public class StandardRuntimeManifestBuilder implements
RuntimeManifestBuilder {
final DependentValues dependentValues =
dependency.getDependentValues();
if (dependentValues != null && dependentValues.getValues() !=
null) {
- final List<String> values = new ArrayList<String>();
- values.addAll(dependentValues.getValues());
+ final List<String> values = new
ArrayList<>(dependentValues.getValues());
propertyDependency.setDependentValues(values);
}
propertyDependencies.add(propertyDependency);
@@ -565,18 +594,15 @@ public class StandardRuntimeManifestBuilder implements
RuntimeManifestBuilder {
}
final PropertyResourceDefinition propertyResourceDefinition = new
PropertyResourceDefinition();
- switch (resourceDefinition.getCardinality()) {
- case SINGLE:
-
propertyResourceDefinition.setCardinality(ResourceCardinality.SINGLE);
- break;
- case MULTIPLE:
-
propertyResourceDefinition.setCardinality(ResourceCardinality.MULTIPLE);
- break;
- }
+ final ResourceCardinality cardinality = switch
(resourceDefinition.getCardinality()) {
+ case SINGLE -> ResourceCardinality.SINGLE;
+ case MULTIPLE -> ResourceCardinality.MULTIPLE;
+ };
+ propertyResourceDefinition.setCardinality(cardinality);
propertyResourceDefinition.setResourceTypes(
resourceDefinition.getResourceTypes().stream()
- .map(rt -> getResourceType(rt))
+ .map(this::getResourceType)
.collect(Collectors.toSet())
);
@@ -584,18 +610,12 @@ public class StandardRuntimeManifestBuilder implements
RuntimeManifestBuilder {
}
private ResourceType getResourceType(final
org.apache.nifi.extension.manifest.ResourceType resourceType) {
- switch (resourceType) {
- case URL:
- return ResourceType.URL;
- case FILE:
- return ResourceType.FILE;
- case TEXT:
- return ResourceType.TEXT;
- case DIRECTORY:
- return ResourceType.DIRECTORY;
- default:
- throw new IllegalArgumentException("Unknown resource type: " +
resourceType);
- }
+ return switch (resourceType) {
+ case URL -> ResourceType.URL;
+ case FILE -> ResourceType.FILE;
+ case TEXT -> ResourceType.TEXT;
+ case DIRECTORY -> ResourceType.DIRECTORY;
+ };
}
private ExpressionLanguageScope getELScope(final
org.apache.nifi.extension.manifest.ExpressionLanguageScope elScope) {
@@ -603,16 +623,11 @@ public class StandardRuntimeManifestBuilder implements
RuntimeManifestBuilder {
return null;
}
- switch (elScope) {
- case NONE:
- return ExpressionLanguageScope.NONE;
- case FLOWFILE_ATTRIBUTES:
- return ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
- case ENVIRONMENT:
- return ExpressionLanguageScope.ENVIRONMENT;
- default:
- throw new IllegalArgumentException("Unknown Expression
Language Scope: " + elScope.name());
- }
+ return switch (elScope) {
+ case NONE -> ExpressionLanguageScope.NONE;
+ case FLOWFILE_ATTRIBUTES ->
ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+ case ENVIRONMENT -> ExpressionLanguageScope.ENVIRONMENT;
+ };
}
private List<PropertyAllowableValue> getPropertyAllowableValues(final
List<AllowableValue> allowableValues) {
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/ClassLoaderAwarePythonBridge.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/ClassLoaderAwarePythonBridge.java
index 8a49feaf92..5679a8f45b 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/ClassLoaderAwarePythonBridge.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/ClassLoaderAwarePythonBridge.java
@@ -96,9 +96,9 @@ public class ClassLoaderAwarePythonBridge implements
PythonBridge {
}
@Override
- public AsyncLoadedProcessor createProcessor(final String identifier, final
String type, final String version, final boolean preferIsolatedProcess) {
+ public AsyncLoadedProcessor createProcessor(final String identifier, final
String type, final String version, final boolean preferIsolatedProcess, final
boolean initialize) {
try (final NarCloseable narCloseable =
NarCloseable.withComponentNarLoader(classLoader)) {
- return delegate.createProcessor(identifier, type, version,
preferIsolatedProcess);
+ return delegate.createProcessor(identifier, type, version,
preferIsolatedProcess, initialize);
}
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
index 13c7609896..c50f968683 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/ExtensionBuilder.java
@@ -866,7 +866,7 @@ public class ExtensionBuilder {
// TODO: This is a hack because there's a bug in the UI that causes
it to not load extensions that don't have a `.` in the type.
final String processorType = type.startsWith("python.") ?
type.substring("python.".length()) : type;
- final Processor processor =
pythonBridge.createProcessor(identifier, processorType,
bundleCoordinate.getVersion(), true);
+ final Processor processor =
pythonBridge.createProcessor(identifier, processorType,
bundleCoordinate.getVersion(), true, true);
final ComponentLog componentLog = new
SimpleProcessLogger(identifier, processor, new StandardLoggingContext(null));
final TerminationAwareLogger terminationAwareLogger = new
TerminationAwareLogger(componentLog);
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/manifest/StandardRuntimeManifestService.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/manifest/StandardRuntimeManifestService.java
index b0851a0767..a6e2b19ba7 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/manifest/StandardRuntimeManifestService.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/manifest/StandardRuntimeManifestService.java
@@ -20,10 +20,29 @@ import org.apache.nifi.bundle.Bundle;
import org.apache.nifi.bundle.BundleDetails;
import org.apache.nifi.c2.protocol.component.api.BuildInfo;
import org.apache.nifi.c2.protocol.component.api.RuntimeManifest;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.extension.manifest.AllowableValue;
+import org.apache.nifi.extension.manifest.ControllerServiceDefinition;
+import org.apache.nifi.extension.manifest.ExpressionLanguageScope;
+import org.apache.nifi.extension.manifest.Extension;
import org.apache.nifi.extension.manifest.ExtensionManifest;
+import org.apache.nifi.extension.manifest.ExtensionType;
+import org.apache.nifi.extension.manifest.InputRequirement;
+import org.apache.nifi.extension.manifest.MultiProcessorUseCase;
+import org.apache.nifi.extension.manifest.ProcessorConfiguration;
+import org.apache.nifi.extension.manifest.Property;
+import org.apache.nifi.extension.manifest.UseCase;
import org.apache.nifi.extension.manifest.parser.ExtensionManifestParser;
+import org.apache.nifi.nar.ExtensionDefinition;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.nar.NarClassLoadersHolder;
+import org.apache.nifi.nar.PythonBundle;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.python.PythonProcessorDetails;
+import
org.apache.nifi.python.processor.documentation.MultiProcessorUseCaseDetails;
+import
org.apache.nifi.python.processor.documentation.ProcessorConfigurationDetails;
+import org.apache.nifi.python.processor.documentation.PropertyDescription;
+import org.apache.nifi.python.processor.documentation.UseCaseDetails;
import org.apache.nifi.runtime.manifest.ExtensionManifestContainer;
import org.apache.nifi.runtime.manifest.RuntimeManifestBuilder;
import org.apache.nifi.runtime.manifest.impl.SchedulingDefaultsFactory;
@@ -33,12 +52,14 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
+import java.util.ArrayList;
import java.util.Date;
+import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -94,16 +115,186 @@ public class StandardRuntimeManifestService implements
RuntimeManifestService {
getExtensionManifest(bundle).ifPresent(manifestBuilder::addBundle);
}
+ getPythonExtensionManifests().forEach(manifestBuilder::addBundle);
+
return manifestBuilder.build();
}
+ private List<ExtensionManifestContainer> getPythonExtensionManifests() {
+ final Map<String, List<Extension>> extensionsPerVersion = new
HashMap<>();
+ final Set<ExtensionDefinition> processorDefinitions =
extensionManager.getExtensions(Processor.class);
+ for (final ExtensionDefinition definition : processorDefinitions) {
+ if
(!PythonBundle.isPythonCoordinate(definition.getBundle().getBundleDetails().getCoordinate()))
{
+ continue;
+ }
+
+ final PythonProcessorDetails pythonProcessorDetails =
extensionManager.getPythonProcessorDetails(definition.getImplementationClassName(),
definition.getVersion());
+ if (pythonProcessorDetails == null) {
+ LOGGER.debug("Could not find Python Processor Details for {}
version {}", definition.getImplementationClassName(), definition.getVersion());
+ continue;
+ }
+
+ final Extension extension =
createExtension(pythonProcessorDetails);
+ final List<Extension> extensions =
extensionsPerVersion.computeIfAbsent(definition.getVersion(), version -> new
ArrayList<>());
+ extensions.add(extension);
+ }
+
+ if (extensionsPerVersion.isEmpty()) {
+ return List.of();
+ }
+
+ final List<ExtensionManifestContainer> containers = new ArrayList<>();
+ for (final Map.Entry<String, List<Extension>> entry :
extensionsPerVersion.entrySet()) {
+ final String version = entry.getKey();
+ final List<Extension> extensions = entry.getValue();
+
+ final ExtensionManifest extensionManifest = new
ExtensionManifest();
+ extensionManifest.setGroupId(PythonBundle.GROUP_ID);
+ extensionManifest.setArtifactId(PythonBundle.ARTIFACT_ID);
+ extensionManifest.setVersion(version);
+ extensionManifest.setExtensions(extensions);
+
+ containers.add(new ExtensionManifestContainer(extensionManifest,
Map.of()));
+ }
+
+ return containers;
+ }
+
+ private Extension createExtension(final PythonProcessorDetails
pythonProcessorDetails) {
+ final Extension extension = new Extension();
+
extension.setDescription(pythonProcessorDetails.getCapabilityDescription());
+ extension.setName(pythonProcessorDetails.getProcessorType());
+ extension.setInputRequirement(InputRequirement.INPUT_REQUIRED);
+ extension.setSupportsBatching(true);
+ extension.setType(ExtensionType.PROCESSOR);
+ extension.setTriggerWhenEmpty(false);
+ extension.setTriggerSerially(false);
+ extension.setTriggerWhenAnyDestinationAvailable(false);
+
+ final List<org.apache.nifi.extension.manifest.Relationship>
relationships = new ArrayList<>();
+ extension.setRelationships(relationships);
+ for (final String relationshipName : List.of("success", "failure",
"original")) {
+ final org.apache.nifi.extension.manifest.Relationship relationship
= new org.apache.nifi.extension.manifest.Relationship();
+ relationship.setAutoTerminated(false);
+ relationship.setName(relationshipName);
+ relationships.add(relationship);
+ }
+
+ final List<UseCase> useCases = getUseCases(pythonProcessorDetails);
+ extension.setUseCases(useCases);
+
+ final List<MultiProcessorUseCase> multiProcessorUseCases =
getMultiProcessorUseCases(pythonProcessorDetails);
+ extension.setMultiProcessorUseCases(multiProcessorUseCases);
+
+ final List<PropertyDescription> propertyDescriptions =
pythonProcessorDetails.getPropertyDescriptions();
+ final List<Property> manifestProperties = propertyDescriptions == null
? List.of() : propertyDescriptions.stream()
+ .map(StandardRuntimeManifestService::createManifestProperty)
+ .toList();
+ extension.setProperties(manifestProperties);
+
+ return extension;
+ }
+
+ private static Property createManifestProperty(final PropertyDescription
propertyDescription) {
+ final Property property = new Property();
+ property.setName(propertyDescription.getName());
+ property.setDescription(propertyDescription.getDescription());
+ property.setDefaultValue(propertyDescription.getDefaultValue());
+ property.setDisplayName(propertyDescription.getDisplayName());
+ property.setDynamicallyModifiesClasspath(false);
+ property.setDynamic(false);
+ try {
+
property.setExpressionLanguageScope(ExpressionLanguageScope.valueOf(propertyDescription.getExpressionLanguageScope()));
+ } catch (final Exception e) {
+ property.setExpressionLanguageScope(ExpressionLanguageScope.NONE);
+ }
+
+ property.setRequired(propertyDescription.isRequired());
+ property.setSensitive(propertyDescription.isSensitive());
+
+ // TODO: Handle Allowable Values
+
property.setControllerServiceDefinition(getManifestControllerServiceDefinition(propertyDescription.getControllerServiceDefinition()));
+
+ return property;
+ }
+
+ private static ControllerServiceDefinition
getManifestControllerServiceDefinition(final String controllerServiceClassName)
{
+ if (controllerServiceClassName == null) {
+ return null;
+ }
+
+ final ControllerServiceDefinition definition = new
ControllerServiceDefinition();
+ definition.setClassName(controllerServiceClassName);
+ return definition;
+ }
+
+ private static List<AllowableValue> getManifestAllowableValues(final
PropertyDescriptor propertyDescriptor) {
+ if (propertyDescriptor.getAllowableValues() == null) {
+ return List.of();
+ }
+
+ final List<AllowableValue> allowableValues = new ArrayList<>();
+ for (final org.apache.nifi.components.AllowableValue allowableValue :
propertyDescriptor.getAllowableValues()) {
+ final AllowableValue manifestAllowableValue = new AllowableValue();
+
manifestAllowableValue.setDescription(allowableValue.getDescription());
+ manifestAllowableValue.setValue(allowableValue.getValue());
+
manifestAllowableValue.setDisplayName(allowableValue.getDisplayName());
+ allowableValues.add(manifestAllowableValue);
+ }
+ return allowableValues;
+ }
+
+
+ private static List<UseCase> getUseCases(final PythonProcessorDetails
pythonProcessorDetails) {
+ final List<UseCase> useCases = new ArrayList<>();
+ for (final UseCaseDetails useCaseDetails :
pythonProcessorDetails.getUseCases()) {
+ final UseCase useCase = new UseCase();
+ useCases.add(useCase);
+
+ useCase.setDescription(useCaseDetails.getDescription());
+ useCase.setNotes(useCaseDetails.getNotes());
+ useCase.setKeywords(useCaseDetails.getKeywords());
+ useCase.setInputRequirement(InputRequirement.INPUT_REQUIRED);
+ useCase.setConfiguration(useCaseDetails.getConfiguration());
+ }
+ return useCases;
+ }
+
+ private static List<MultiProcessorUseCase> getMultiProcessorUseCases(final
PythonProcessorDetails pythonProcessorDetails) {
+ final List<MultiProcessorUseCase> multiProcessorUseCases = new
ArrayList<>();
+ for (final MultiProcessorUseCaseDetails useCaseDetails :
pythonProcessorDetails.getMultiProcessorUseCases()) {
+ final MultiProcessorUseCase useCase = new MultiProcessorUseCase();
+ multiProcessorUseCases.add(useCase);
+
+ useCase.setDescription(useCaseDetails.getDescription());
+ useCase.setNotes(useCaseDetails.getNotes());
+ useCase.setKeywords(useCaseDetails.getKeywords());
+
+ final List<ProcessorConfiguration> processorConfigurations = new
ArrayList<>();
+ useCase.setProcessorConfigurations(processorConfigurations);
+ for (final ProcessorConfigurationDetails processorConfig :
useCaseDetails.getConfigurations()) {
+ final ProcessorConfiguration processorConfiguration = new
ProcessorConfiguration();
+ processorConfigurations.add(processorConfiguration);
+
+
processorConfiguration.setConfiguration(processorConfig.getConfiguration());
+
processorConfiguration.setProcessorClassName(processorConfig.getProcessorType());
+ }
+ }
+
+ return multiProcessorUseCases;
+ }
+
private Optional<ExtensionManifestContainer> getExtensionManifest(final
Bundle bundle) {
final BundleDetails bundleDetails = bundle.getBundleDetails();
try {
- final ExtensionManifest extensionManifest =
loadExtensionManifest(bundleDetails);
+ final Optional<ExtensionManifest> extensionManifest =
loadExtensionManifest(bundleDetails);
+ if (extensionManifest.isEmpty()) {
+ return Optional.empty();
+ }
+
final Map<String, String> additionalDetails =
loadAdditionalDetails(bundleDetails);
- final ExtensionManifestContainer container = new
ExtensionManifestContainer(extensionManifest, additionalDetails);
+ final ExtensionManifestContainer container = new
ExtensionManifestContainer(extensionManifest.get(), additionalDetails);
return Optional.of(container);
} catch (final IOException e) {
LOGGER.error("Unable to load extension manifest for bundle [{}]",
bundleDetails.getCoordinate(), e);
@@ -111,11 +302,11 @@ public class StandardRuntimeManifestService implements
RuntimeManifestService {
}
}
- private ExtensionManifest loadExtensionManifest(final BundleDetails
bundleDetails) throws IOException {
+ private Optional<ExtensionManifest> loadExtensionManifest(final
BundleDetails bundleDetails) throws IOException {
final File manifestFile = new
File(bundleDetails.getWorkingDirectory(),
"META-INF/docs/extension-manifest.xml");
if (!manifestFile.exists()) {
- throw new FileNotFoundException("Extension manifest files does not
exist for "
- + bundleDetails.getCoordinate() + " at " +
manifestFile.getAbsolutePath());
+ LOGGER.warn("There is no extension manifest for bundle [{}]",
bundleDetails.getCoordinate());
+ return Optional.empty();
}
try (final InputStream inputStream = new
FileInputStream(manifestFile)) {
@@ -125,7 +316,7 @@ public class StandardRuntimeManifestService implements
RuntimeManifestService {
extensionManifest.setGroupId(bundleDetails.getCoordinate().getGroup());
extensionManifest.setArtifactId(bundleDetails.getCoordinate().getId());
extensionManifest.setVersion(bundleDetails.getCoordinate().getVersion());
- return extensionManifest;
+ return Optional.of(extensionManifest);
}
}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/ExtensionDefinition.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/ExtensionDefinition.java
index ba266680e0..29adda1201 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/ExtensionDefinition.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/ExtensionDefinition.java
@@ -180,7 +180,7 @@ public class ExtensionDefinition {
}
}
- enum ExtensionRuntime {
+ public enum ExtensionRuntime {
JAVA,
PYTHON;
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/ExtensionManager.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/ExtensionManager.java
index aa6a0c9b1f..a6c308f0d9 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/ExtensionManager.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/ExtensionManager.java
@@ -19,6 +19,7 @@ package org.apache.nifi.nar;
import org.apache.nifi.bundle.Bundle;
import org.apache.nifi.bundle.BundleCoordinate;
import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.python.PythonProcessorDetails;
import java.net.URL;
import java.util.List;
@@ -151,6 +152,14 @@ public interface ExtensionManager {
*/
ConfigurableComponent getTempComponent(String classType, BundleCoordinate
bundleCoordinate);
+ /**
+ * Returns the details about the Python Processor with the given type and
version
+ * @param processorType the type of the Processor
+ * @param version the version of the Processor
+ * @return the details for the Python Processor, or <code>null</code> if
no Processor can be given that match the given type and version
+ */
+ PythonProcessorDetails getPythonProcessorDetails(String processorType,
String version);
+
/**
* Logs the available class loaders.
*/
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/StandardExtensionDiscoveringManager.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/StandardExtensionDiscoveringManager.java
index bed296e3c2..3931f46898 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/StandardExtensionDiscoveringManager.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/StandardExtensionDiscoveringManager.java
@@ -96,6 +96,7 @@ public class StandardExtensionDiscoveringManager implements
ExtensionDiscovering
private final Map<BundleCoordinate, Bundle> bundleCoordinateBundleLookup =
new HashMap<>();
private final Map<ClassLoader, Bundle> classLoaderBundleLookup = new
HashMap<>();
private final Map<String, ConfigurableComponent> tempComponentLookup = new
HashMap<>();
+ private final Map<String, List<PythonProcessorDetails>>
pythonProcessorDetails = new HashMap<>();
private final Map<String, InstanceClassLoader> instanceClassloaderLookup =
new ConcurrentHashMap<>();
private final ConcurrentMap<BaseClassLoaderKey, SharedInstanceClassLoader>
sharedBaseClassloaders = new ConcurrentHashMap<>();
@@ -201,7 +202,7 @@ public class StandardExtensionDiscoveringManager implements
ExtensionDiscovering
final Bundle bundle = new Bundle(bundleDetails,
pythonBundle.getClassLoader());
// TODO: This is a workaround because the UI has a bug that causes
it not to work properly if the type doesn't have a '.' in it
- final String className = "python." + details.getProcessorType();
+ final String className = PYTHON_TYPE_PREFIX +
details.getProcessorType();
final ExtensionDefinition extensionDefinition = new
ExtensionDefinition.Builder()
.implementationClassName(className)
.runtime(ExtensionRuntime.PYTHON)
@@ -218,6 +219,10 @@ public class StandardExtensionDiscoveringManager
implements ExtensionDiscovering
final List<Bundle> bundlesForClass =
classNameBundleLookup.computeIfAbsent(className, key -> new ArrayList<>());
bundlesForClass.add(bundle);
bundleCoordinateBundleLookup.putIfAbsent(bundleDetails.getCoordinate(), bundle);
+
+ final List<PythonProcessorDetails> detailsList =
this.pythonProcessorDetails.computeIfAbsent(details.getProcessorType(), key ->
new ArrayList<>());
+ detailsList.add(details);
+
logger.info("Discovered Python Processor {}",
details.getProcessorType());
} else {
logger.debug("Python Processor {} is already known",
details.getProcessorType());
@@ -231,6 +236,23 @@ public class StandardExtensionDiscoveringManager
implements ExtensionDiscovering
}
}
+ @Override
+ public PythonProcessorDetails getPythonProcessorDetails(final String
processorType, final String version) {
+ final String canonicalProcessorType =
stripPythonTypePrefix(processorType);
+ final List<PythonProcessorDetails> detailsList =
this.pythonProcessorDetails.get(canonicalProcessorType);
+ if (detailsList == null) {
+ return null;
+ }
+
+ for (final PythonProcessorDetails processorDetails : detailsList) {
+ if (processorDetails.getProcessorVersion().equals(version)) {
+ return processorDetails;
+ }
+ }
+
+ return null;
+ }
+
private BundleDetails createBundleDetailsWithOverriddenVersion(final
BundleDetails details, final String version) {
final BundleCoordinate overriddenCoordinate = new
BundleCoordinate(details.getCoordinate().getGroup(),
details.getCoordinate().getId(), version);
@@ -705,10 +727,10 @@ public class StandardExtensionDiscoveringManager
implements ExtensionDiscovering
final ConfigurableComponent tempComponent;
if
(PythonBundle.isPythonCoordinate(bundle.getBundleDetails().getCoordinate())) {
// TODO: This is a workaround due to bug in UI. Fix bug in UI.
- final String type = classType.startsWith(PYTHON_TYPE_PREFIX) ?
classType.substring(PYTHON_TYPE_PREFIX.length()) : classType;
+ final String type = stripPythonTypePrefix(classType);
final String procId = "temp-component-" + type;
- tempComponent = pythonBridge.createProcessor(procId, type,
bundleCoordinate.getVersion(), false);
+ tempComponent = pythonBridge.createProcessor(procId, type,
bundleCoordinate.getVersion(), false, false);
} else {
final Class<?> componentClass = Class.forName(classType, true,
bundleClassLoader);
tempComponent = (ConfigurableComponent)
componentClass.getDeclaredConstructor().newInstance();
@@ -729,6 +751,16 @@ public class StandardExtensionDiscoveringManager
implements ExtensionDiscovering
}
}
+ private static String stripPythonTypePrefix(final String value) {
+ if (value == null) {
+ return null;
+ }
+ if (value.startsWith(PYTHON_TYPE_PREFIX)) {
+ return value.substring(PYTHON_TYPE_PREFIX.length());
+ }
+
+ return value;
+ }
private static String getClassBundleKey(final String classType, final
BundleCoordinate bundleCoordinate) {
return classType + "_" + bundleCoordinate.getCoordinate();
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/nar/PythonBundle.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/nar/PythonBundle.java
index 6c3c65174a..ad21ae4d56 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/nar/PythonBundle.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/nar/PythonBundle.java
@@ -26,8 +26,11 @@ import java.io.File;
import java.util.Objects;
public class PythonBundle {
- private static final BundleCoordinate PYTHON_BUNDLE_COORDINATE = new
BundleCoordinate(
- "org.apache.nifi", "python-extensions",
BundleCoordinate.DEFAULT_VERSION);
+ public static final String GROUP_ID = "org.apache.nifi";
+ public static final String ARTIFACT_ID = "python-extensions";
+ public static final String VERSION = BundleCoordinate.DEFAULT_VERSION;
+
+ private static final BundleCoordinate PYTHON_BUNDLE_COORDINATE = new
BundleCoordinate(GROUP_ID, ARTIFACT_ID, VERSION);
public static Bundle create(final NiFiProperties properties, final
ClassLoader classLoader) {
final File pythonWorkingDirectory = new
File(properties.getProperty(NiFiProperties.PYTHON_WORKING_DIRECTORY,
NiFiProperties.DEFAULT_PYTHON_WORKING_DIRECTORY));
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/py4j/StandardPythonBridge.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/py4j/StandardPythonBridge.java
index 578d3b887e..eb568dfdad 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/py4j/StandardPythonBridge.java
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/py4j/StandardPythonBridge.java
@@ -132,20 +132,21 @@ public class StandardPythonBridge implements PythonBridge
{
}
@Override
- public AsyncLoadedProcessor createProcessor(final String identifier, final
String type, final String version, final boolean preferIsolatedProcess) {
+ public AsyncLoadedProcessor createProcessor(final String identifier, final
String type, final String version, final boolean preferIsolatedProcess, final
boolean initialize) {
final PythonProcessorDetails processorDetails =
getProcessorTypes().stream()
.filter(details -> details.getProcessorType().equals(type))
+ .filter(details -> details.getProcessorVersion().equals(version))
.findFirst()
- .orElseThrow(() -> new IllegalArgumentException("Unknown Python
Processor type: " + type));
+ .orElseThrow(() -> new IllegalArgumentException("Unknown Python
Processor type [%s] or version [%s]".formatted(type, version)));
final String implementedInterface = processorDetails.getInterface();
final Supplier<PythonProcessorBridge> processorBridgeFactory = () ->
createProcessorBridge(identifier, type, version, preferIsolatedProcess);
if (FlowFileTransform.class.getName().equals(implementedInterface)) {
- return new FlowFileTransformProxy(type, processorBridgeFactory);
+ return new FlowFileTransformProxy(type, processorBridgeFactory,
initialize);
}
if (RecordTransform.class.getName().equals(implementedInterface)) {
- return new RecordTransformProxy(type, processorBridgeFactory);
+ return new RecordTransformProxy(type, processorBridgeFactory,
initialize);
}
return null;
}
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/py4j/StandardPythonProcessorBridge.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/py4j/StandardPythonProcessorBridge.java
index 56b61a7727..848f2c3d0f 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/py4j/StandardPythonProcessorBridge.java
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/py4j/StandardPythonProcessorBridge.java
@@ -117,7 +117,7 @@ public class StandardPythonProcessorBridge implements
PythonProcessorBridge {
}
sleepMillis = Math.min(sleepMillis * 2,
TimeUnit.MINUTES.toMillis(10));
- logger.error("Failed to load code for Python Processor {}
({}). Will try again in {} millis", initializationContext.getIdentifier(),
getProcessorType(), sleepMillis);
+ logger.error("Failed to load code for Python Processor {}
({}). Will try again in {} millis", initializationContext.getIdentifier(),
getProcessorType(), sleepMillis, e);
try {
Thread.sleep(sleepMillis);
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/FlowFileTransformProxy.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/FlowFileTransformProxy.java
index 0af61d738a..06f54472a0 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/FlowFileTransformProxy.java
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/FlowFileTransformProxy.java
@@ -36,8 +36,8 @@ public class FlowFileTransformProxy extends
PythonProcessorProxy {
private volatile FlowFileTransform transform;
- public FlowFileTransformProxy(final String processorType, final
Supplier<PythonProcessorBridge> bridgeFactory) {
- super(processorType, bridgeFactory);
+ public FlowFileTransformProxy(final String processorType, final
Supplier<PythonProcessorBridge> bridgeFactory, final boolean initialize) {
+ super(processorType, bridgeFactory, initialize);
}
@OnScheduled
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/PythonProcessorProxy.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/PythonProcessorProxy.java
index 1b9383819e..9ee4fcb212 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/PythonProcessorProxy.java
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/PythonProcessorProxy.java
@@ -66,16 +66,18 @@ public abstract class PythonProcessorProxy extends
AbstractProcessor implements
REL_ORIGINAL,
REL_FAILURE);
- public PythonProcessorProxy(final String processorType, final
Supplier<PythonProcessorBridge> bridgeFactory) {
+ public PythonProcessorProxy(final String processorType, final
Supplier<PythonProcessorBridge> bridgeFactory, final boolean initialize) {
this.processorType = processorType;
Thread.ofVirtual().name("Initialize " + processorType).start(() -> {
this.bridge = bridgeFactory.get();
- // If initialization context has already been set, initialize
bridge.
- final PythonProcessorInitializationContext pythonInitContext =
initContext;
- if (pythonInitContext != null) {
- this.bridge.initialize(pythonInitContext);
+ if (initialize) {
+ // If initialization context has already been set, initialize
bridge.
+ final PythonProcessorInitializationContext pythonInitContext =
initContext;
+ if (pythonInitContext != null) {
+ this.bridge.initialize(pythonInitContext);
+ }
}
});
}
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/RecordTransformProxy.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/RecordTransformProxy.java
index ecae852962..4f20c56850 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/RecordTransformProxy.java
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-bridge/src/main/java/org/apache/nifi/python/processor/RecordTransformProxy.java
@@ -78,8 +78,8 @@ public class RecordTransformProxy extends
PythonProcessorProxy {
.build();
- public RecordTransformProxy(final String processorType, final
Supplier<PythonProcessorBridge> bridgeFactory) {
- super(processorType, bridgeFactory);
+ public RecordTransformProxy(final String processorType, final
Supplier<PythonProcessorBridge> bridgeFactory, final boolean initialize) {
+ super(processorType, bridgeFactory, initialize);
}
@Override
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-integration-tests/src/test/java/org.apache.nifi.py4j/PythonControllerInteractionIT.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-integration-tests/src/test/java/org.apache.nifi.py4j/PythonControllerInteractionIT.java
index 30b7e9e1b4..846adb64c4 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-integration-tests/src/test/java/org.apache.nifi.py4j/PythonControllerInteractionIT.java
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-py4j-integration-tests/src/test/java/org.apache.nifi.py4j/PythonControllerInteractionIT.java
@@ -567,7 +567,7 @@ public class PythonControllerInteractionIT {
private Processor createProcessor(final String type, final String version)
{
bridge.discoverExtensions();
- final AsyncLoadedProcessor processor =
bridge.createProcessor(createId(), type, version, true);
+ final AsyncLoadedProcessor processor =
bridge.createProcessor(createId(), type, version, true, true);
final ProcessorInitializationContext initContext = new
MockProcessorInitializationContext();
processor.initialize(initContext);
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/__jvm__.py
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/__jvm__.py
index bc396f9f63..c4af4691fd 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/__jvm__.py
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/__jvm__.py
@@ -14,4 +14,10 @@
# limitations under the License.
class JvmHolder:
- jvm = None
\ No newline at end of file
+ jvm = None
+
+def ArrayList(list: list):
+ array_list = JvmHolder.jvm.java.util.ArrayList()
+ for val in list:
+ array_list.add(val)
+ return array_list
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/documentation.py
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/documentation.py
new file mode 100644
index 0000000000..39b031657f
--- /dev/null
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/documentation.py
@@ -0,0 +1,142 @@
+# 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.__jvm__ import ArrayList
+
+class ProcessorConfiguration:
+ class Java:
+ implements =
['org.apache.nifi.python.processor.documentation.ProcessorConfigurationDetails']
+
+ def __init__(self, processor_type: str, configuration: str):
+ self.processor_type = processor_type
+ self.configuration = configuration
+
+ def getProcessorType(self):
+ return self.processor_type
+
+ def getConfiguration(self):
+ return self.configuration
+
+
+def use_case(description: str, configuration: str = None, notes: str = None,
keywords: list[str] = None):
+ """Decorator to explain how to perform a specific use case with a given
processor"""
+ def decorator(func):
+ return func
+ return decorator
+
+
+def multi_processor_use_case(description: str, configurations:
list[ProcessorConfiguration], notes: str = None, keywords: list[str] = None):
+ """Decorator to explain how to perform a specific use case that involves
the decorated Processor, in addition to additional Processors"""
+ def decorator(func):
+ return func
+ return decorator
+
+
+class UseCaseDetails:
+ class Java:
+ implements =
['org.apache.nifi.python.processor.documentation.UseCaseDetails']
+
+ def __init__(self, description: str, notes: str, keywords: list[str],
configuration: str):
+ self.description = description
+ self.notes = notes
+ self.keywords = keywords
+ self.configuration = configuration
+
+ def getDescription(self):
+ return self.description
+
+ def getNotes(self):
+ return self.notes
+
+ def getKeywords(self):
+ return ArrayList(self.keywords)
+
+ def getConfiguration(self):
+ return self.configuration
+
+ def __str__(self):
+ return f"UseCaseDetails[description={self.description}]"
+
+
+class MultiProcessorUseCaseDetails:
+ class Java:
+ implements =
['org.apache.nifi.python.processor.documentation.MultiProcessorUseCaseDetails']
+
+ def __init__(self, description: str, notes: str, keywords: list[str],
configurations: list[ProcessorConfiguration]):
+ self.description = description
+ self.notes = notes
+ self.keywords = keywords
+ self.configurations = configurations
+
+ def getDescription(self):
+ return self.description
+
+ def getNotes(self):
+ return self.notes
+
+ def getKeywords(self):
+ return ArrayList(self.keywords)
+
+ def getConfigurations(self):
+ return ArrayList(self.configurations)
+
+ def __str__(self):
+ return f"MultiProcessorUseCaseDetails[description={self.description}]"
+
+class PropertyDescription:
+ class Java:
+ implements =
['org.apache.nifi.python.processor.documentation.PropertyDescription']
+
+ def __init__(self,
+ name: str,
+ description: str,
+ display_name: str = None,
+ required: bool = False,
+ sensitive: bool = True,
+ default_value: str = None,
+ expression_language_scope: str = None,
+ controller_service_definition: str = None):
+ self.name = name
+ self.description = description
+ self.display_name = display_name
+ self.required = required
+ self.sensitive = sensitive
+ self.default_value = default_value
+ self.expression_language_scope = expression_language_scope
+ self.controller_service_definition = controller_service_definition
+
+ def getName(self):
+ return self.name
+
+ def getDescription(self):
+ return self.description
+
+ def getDisplayName(self):
+ return self.display_name
+
+ def isRequired(self):
+ return self.required
+
+ def isSensitive(self):
+ return self.sensitive
+
+ def getDefaultValue(self):
+ return self.default_value
+
+ def getExpressionLanguageScope(self):
+ return self.expression_language_scope
+
+ def getControllerServiceDefinition(self):
+ return self.controller_service_definition
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/DisabledPythonBridge.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/DisabledPythonBridge.java
index 2911a84839..2071a9c916 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/DisabledPythonBridge.java
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/DisabledPythonBridge.java
@@ -64,7 +64,7 @@ public class DisabledPythonBridge implements PythonBridge {
}
@Override
- public AsyncLoadedProcessor createProcessor(final String identifier, final
String type, final String version, final boolean preferIsolatedProcess) {
+ public AsyncLoadedProcessor createProcessor(final String identifier, final
String type, final String version, final boolean preferIsolatedProcess, final
boolean initialize) {
throw new UnsupportedOperationException("Cannot create Processor of
type " + type + " because Python extensions are disabled");
}
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/PythonBridge.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/PythonBridge.java
index 20d2c1f180..a75ebca5dd 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/PythonBridge.java
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/PythonBridge.java
@@ -94,9 +94,10 @@ public interface PythonBridge {
* @param type the Processor's type
* @param version the Processor's version
* @param preferIsolatedProcess whether or not to prefer launching a
Python Process that is isolated for just this one instance of the Processor
+ * @param initialize whether or not to initialize the processor
* @return a PythonProcessorBridge that can be used for interacting with
the Processor
*/
- AsyncLoadedProcessor createProcessor(String identifier, String type,
String version, boolean preferIsolatedProcess);
+ AsyncLoadedProcessor createProcessor(String identifier, String type,
String version, boolean preferIsolatedProcess, boolean initialize);
/**
* A notification that the Processor with the given identifier, type, and
version was removed from the flow. This triggers the bridge
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/PythonProcessorDetails.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/PythonProcessorDetails.java
index aed38f92fa..cb8237ceed 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/PythonProcessorDetails.java
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/PythonProcessorDetails.java
@@ -17,6 +17,10 @@
package org.apache.nifi.python;
+import
org.apache.nifi.python.processor.documentation.MultiProcessorUseCaseDetails;
+import org.apache.nifi.python.processor.documentation.PropertyDescription;
+import org.apache.nifi.python.processor.documentation.UseCaseDetails;
+
import java.util.List;
public interface PythonProcessorDetails {
@@ -54,4 +58,22 @@ public interface PythonProcessorDetails {
* @return the name of the Java interface that is implemented by the
Python Processor
*/
String getInterface();
+
+ /**
+ * @return the use cases that have been described by the Python Processor
+ */
+ List<UseCaseDetails> getUseCases();
+
+ /**
+ * @return the multi-processor use cases that have been described by the
Python Processor
+ */
+ List<MultiProcessorUseCaseDetails> getMultiProcessorUseCases();
+
+ /**
+ * A list of property descriptions for the known properties. Note that
unlikely Java Processors, Python-based Processors are
+ * more dynamic, and the properties may not all be discoverable
+ *
+ * @return a list of descriptions for known properties
+ */
+ List<PropertyDescription> getPropertyDescriptions();
}
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/MultiProcessorUseCaseDetails.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/MultiProcessorUseCaseDetails.java
new file mode 100644
index 0000000000..b772ca992d
--- /dev/null
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/MultiProcessorUseCaseDetails.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+package org.apache.nifi.python.processor.documentation;
+
+import java.util.List;
+
+public interface MultiProcessorUseCaseDetails {
+ String getDescription();
+
+ String getNotes();
+
+ List<String> getKeywords();
+
+ List<ProcessorConfigurationDetails> getConfigurations();
+}
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/ProcessorConfigurationDetails.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/ProcessorConfigurationDetails.java
new file mode 100644
index 0000000000..d32b3f01ab
--- /dev/null
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/ProcessorConfigurationDetails.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+package org.apache.nifi.python.processor.documentation;
+
+public interface ProcessorConfigurationDetails {
+ String getProcessorType();
+
+ String getConfiguration();
+}
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/PropertyDescription.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/PropertyDescription.java
new file mode 100644
index 0000000000..f843f85642
--- /dev/null
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/PropertyDescription.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+package org.apache.nifi.python.processor.documentation;
+
+public interface PropertyDescription {
+ String getName();
+
+ String getDisplayName();
+
+ String getDescription();
+
+ String getDefaultValue();
+
+ boolean isSensitive();
+
+ boolean isRequired();
+
+ String getExpressionLanguageScope();
+
+ String getControllerServiceDefinition();
+}
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/UseCaseDetails.java
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/UseCaseDetails.java
new file mode 100644
index 0000000000..307f63aa49
--- /dev/null
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework-api/src/main/java/org/apache/nifi/python/processor/documentation/UseCaseDetails.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+
+package org.apache.nifi.python.processor.documentation;
+
+import java.util.List;
+
+public interface UseCaseDetails {
+ String getDescription();
+
+ String getNotes();
+
+ List<String> getKeywords();
+
+ String getConfiguration();
+}
diff --git a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/pom.xml
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/pom.xml
index 5ebff16d99..ceec25b200 100644
--- a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/pom.xml
+++ b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/pom.xml
@@ -76,4 +76,46 @@
</plugins>
</build>
+ <profiles>
+ <profile>
+ <id>python-unit-tests</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <version>3.1.1</version>
+ <executions>
+ <execution>
+ <id>python-test</id>
+ <phase>test</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+ <executable>python3</executable>
+ <environmentVariables>
+
<PYTHONPATH>src/main/python/framework:../nifi-python-extension-api/src/main/python/src</PYTHONPATH>
+ </environmentVariables>
+ <arguments>
+ <argument>-m</argument>
+ <argument>unittest</argument>
+ <argument>discover</argument>
+ <argument>-s</argument>
+
<argument>src/test/python/framework</argument>
+ <argument>-p</argument>
+ <argument>Test*.py</argument>
+ </arguments>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
</project>
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/ExtensionDetails.py
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/ExtensionDetails.py
new file mode 100644
index 0000000000..fbab6dca5d
--- /dev/null
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/ExtensionDetails.py
@@ -0,0 +1,74 @@
+# 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.__jvm__ import ArrayList
+
+class ExtensionDetails:
+ class Java:
+ implements = ['org.apache.nifi.python.PythonProcessorDetails']
+
+ def __init__(self, type, interfaces,
+ version='Unknown',
+ dependencies=None,
+ source_location=None,
+ description=None,
+ tags=None,
+ use_cases=None,
+ multi_processor_use_cases=None,
+ property_descriptions=None):
+
+ self.type = type
+ self.interfaces = interfaces if interfaces else []
+ self.dependencies = dependencies if dependencies else []
+ self.tags = tags if tags else []
+ self.version = version
+ self.source_location = source_location
+ self.description = description
+ self.use_cases = use_cases if use_cases else {}
+ self.multi_processor_use_cases = multi_processor_use_cases if
multi_processor_use_cases else {}
+ self.property_descriptions = property_descriptions if
property_descriptions else {}
+
+ def getProcessorType(self):
+ return self.type
+
+ def getProcessorVersion(self):
+ return self.version
+
+ def getSourceLocation(self):
+ return self.source_location
+
+ def getDependencies(self):
+ return ArrayList(self.dependencies)
+
+ def getCapabilityDescription(self):
+ return self.description
+
+ def getTags(self):
+ return ArrayList(self.tags)
+
+ def getUseCases(self):
+ return ArrayList(self.use_cases)
+
+ def getMultiProcessorUseCases(self):
+ return ArrayList(self.multi_processor_use_cases)
+
+ def getPropertyDescriptions(self):
+ return ArrayList(self.property_descriptions)
+
+ def getInterface(self):
+ if len(self.interfaces) == 0:
+ return None
+ return self.interfaces[0]
+
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/ExtensionManager.py
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/ExtensionManager.py
index 81e641bfa1..2e9063dda3 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/ExtensionManager.py
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/ExtensionManager.py
@@ -13,9 +13,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-import ast
import importlib
-import importlib.util # Note requires Python 3.4+
+import importlib.util
import inspect
import logging
import os
@@ -24,7 +23,10 @@ import subprocess
import sys
from pathlib import Path
-logger = logging.getLogger("org.apache.nifi.py4j.ExtensionManager")
+import ProcessorInspection
+
+
+logger = logging.getLogger("python.ExtensionManager")
# A simple wrapper class to encompass a processor type and its version
@@ -40,67 +42,6 @@ class ExtensionId:
return (self.classname, self.version) == (other.classname,
other.version)
-class ExtensionDetails:
- class Java:
- implements = ['org.apache.nifi.python.PythonProcessorDetails']
-
- def __init__(self, gateway, type, interfaces,
- version='Unknown',
- dependencies=None,
- source_location=None,
- description=None,
- tags=None):
-
- self.gateway = gateway
- self.type = type
-
- if interfaces is None:
- interfaces = []
- self.interfaces = interfaces
-
- if dependencies is None:
- dependencies = []
- if tags is None:
- tags = []
-
- self.version = version
- self.dependencies = dependencies
- self.source_location = source_location
- self.description = description
- self.tags = tags
-
- def getProcessorType(self):
- return self.type
-
- def getProcessorVersion(self):
- return self.version
-
- def getSourceLocation(self):
- return self.source_location
-
- def getDependencies(self):
- list = self.gateway.jvm.java.util.ArrayList()
- for dep in self.dependencies:
- list.add(dep)
-
- return list
-
- def getCapabilityDescription(self):
- return self.description
-
- def getTags(self):
- list = self.gateway.jvm.java.util.ArrayList()
- for tag in self.tags:
- list.add(tag)
-
- return list
-
- def getInterface(self):
- if len(self.interfaces) == 0:
- return None
- return self.interfaces[0]
-
-
class ExtensionManager:
"""
@@ -113,7 +54,7 @@ class ExtensionManager:
third-party dependencies have been imported.
"""
- processorInterfaces =
['org.apache.nifi.python.processor.FlowFileTransform',
'org.apache.nifi.python.processor.RecordTransform']
+ processor_interfaces =
['org.apache.nifi.python.processor.FlowFileTransform',
'org.apache.nifi.python.processor.RecordTransform']
processor_details = {}
processor_class_by_name = {}
module_files_by_extension_type = {}
@@ -307,166 +248,17 @@ class ExtensionManager:
return self.processor_details[id].dependencies
def __get_processor_classes_and_details(self, module_file):
- # Parse the python file
- with open(module_file) as file:
- root_node = ast.parse(file.read())
-
+ class_nodes =
ProcessorInspection.get_processor_class_nodes(module_file)
details_by_class = {}
- # Get top-level class nodes (e.g., MyProcessor)
- class_nodes = self.__get_class_nodes(root_node)
-
for class_node in class_nodes:
- logger.debug(f"Checking if class {class_node.name} is a processor")
- if self.__is_processor_class_node(class_node):
- logger.info(f"Discovered Processor class {class_node.name} in
module {module_file}")
- details = self.__get_processor_details(class_node, module_file)
- details_by_class[class_node.name] = details
+ logger.info(f"Discovered Processor class {class_node.name} in
module {module_file}")
+ details = ProcessorInspection.get_processor_details(class_node,
module_file)
+ details_by_class[class_node.name] = details
return details_by_class
- def __is_processor_class_node(self, class_node):
- """
- Checks if the Abstract Syntax Tree (AST) Node represents a Processor
class.
- We are looking for any classes within the given module file that look
like:
-
- class MyProcessor:
- ...
- class Java:
- implements =
['org.apache.nifi.python.processor.FlowFileTransform']
-
- :param class_node: the abstract syntax tree (AST) node
- :return: True if the AST Node represents a Python Class that is a
Processor, False otherwise
- """
-
- # Look for a 'Java' sub-class
- interfaces = self.__get_java_interfaces(class_node)
- return len(interfaces) > 0
-
-
- def __get_java_interfaces(self, class_node):
- # Get all class definition nodes
- child_class_nodes = self.__get_class_nodes(class_node)
-
- interfaces = []
- for child_class_node in child_class_nodes:
- # Look for a 'Java' sub-class
- if child_class_node.name != 'Java':
- continue
-
- # Look for an assignment that assigns values to the `implements`
keyword
- assignment_nodes = self.__get_assignment_nodes(child_class_node)
- for assignment_node in assignment_nodes:
- targets = assignment_node.targets
- if (len(targets) == 1 and targets[0].id == 'implements'):
- assigned_values = assignment_node.value.elts
- for assigned_value in assigned_values:
- if assigned_value.value in self.processorInterfaces:
- interfaces.append(assigned_value.value)
-
- return interfaces
-
-
- def __get_processor_details(self, class_node, module_file):
- # Look for a 'ProcessorDetails' class
- child_class_nodes = self.__get_class_nodes(class_node)
-
- # Get the Java interfaces that it implements
- interfaces = self.__get_java_interfaces(class_node)
-
- for child_class_node in child_class_nodes:
- if child_class_node.name == 'ProcessorDetails':
- logger.debug(f"Found ProcessorDetails class in
{class_node.name}")
- version = self.__get_processor_version(child_class_node,
class_node.name)
- dependencies =
self.__get_processor_dependencies(child_class_node, class_node.name)
- description =
self.__get_processor_description(child_class_node, class_node.name)
- tags = self.__get_processor_tags(child_class_node,
class_node.name)
-
- return ExtensionDetails(gateway=self.gateway,
- interfaces=interfaces,
- type=class_node.name,
- version=version,
- dependencies=dependencies,
- source_location=module_file,
- description=description,
- tags=tags)
-
- return ExtensionDetails(gateway=self.gateway,
- interfaces=interfaces,
- type=class_node.name,
- version='Unknown',
- dependencies=[],
- source_location=module_file)
-
-
- def __get_processor_version(self, details_node, class_name):
- assignment_nodes = self.__get_assignment_nodes(details_node)
- for assignment_node in assignment_nodes:
- targets = assignment_node.targets
- if (len(targets) == 1 and targets[0].id == 'version'):
- assigned_values = assignment_node.value.value
- logger.info("Found version of {0} to be
{1}".format(class_name, assigned_values))
- return assigned_values
-
- # No dependencies found
- logger.info("Found no version information for {0}".format(class_name))
- return 'Unknown'
-
-
- def __get_processor_dependencies(self, details_node, class_name):
- deps = self.__get_assigned_list(details_node, class_name,
'dependencies')
- if len(deps) == 0:
- logger.info("Found no external dependencies that are required for
class %s" % class_name)
- else:
- logger.info("Found the following external dependencies that are
required for class {0}: {1}".format(class_name, deps))
-
- return deps
-
-
- def __get_processor_tags(self, details_node, class_name):
- return self.__get_assigned_list(details_node, class_name, 'tags')
-
-
- def __get_assigned_list(self, details_node, class_name, element_name):
- assignment_nodes = self.__get_assignment_nodes(details_node)
- for assignment_node in assignment_nodes:
- targets = assignment_node.targets
- if (len(targets) == 1 and targets[0].id == element_name):
- assigned_values = assignment_node.value.elts
- declared_dependencies = []
- for assigned_value in assigned_values:
- declared_dependencies.append(assigned_value.value)
-
- return declared_dependencies
-
- # No values found
- return []
-
-
- def __get_processor_description(self, details_node, class_name):
- assignment_nodes = self.__get_assignment_nodes(details_node)
- for assignment_node in assignment_nodes:
- targets = assignment_node.targets
- if (len(targets) == 1 and targets[0].id == 'description'):
- return assignment_node.value.value
-
- # No description found
- logger.debug("Found no description for class %s" % class_name)
- return None
-
-
-
- def __get_class_nodes(self, node):
- class_nodes = [n for n in node.body if isinstance(n, ast.ClassDef)]
- return class_nodes
-
-
- def __get_assignment_nodes(self, node):
- assignment_nodes = [n for n in node.body if isinstance(n, ast.Assign)]
- return assignment_nodes
-
-
def import_external_dependencies(self, processor_details, work_dir):
class_name = processor_details.getProcessorType()
@@ -519,7 +311,7 @@ class ExtensionManager:
def __load_extension_module(self, file, local_dependencies):
# If there are any local dependencies (i.e., other python files in the
same directory), load those modules first
- if local_dependencies is not None and len(local_dependencies) > 0:
+ if local_dependencies:
to_load = [dep for dep in local_dependencies]
if file in to_load:
to_load.remove(file)
@@ -553,20 +345,20 @@ class ExtensionManager:
to_load.insert(0, local_dependency)
# Determine the module name
- moduleName = Path(file).name.split('.py')[0]
+ module_name = Path(file).name.split('.py')[0]
# Create the module specification
- moduleSpec = importlib.util.spec_from_file_location(moduleName, file)
- logger.debug(f"Module Spec: {moduleSpec}")
+ module_spec = importlib.util.spec_from_file_location(module_name, file)
+ logger.debug(f"Module Spec: {module_spec}")
# Create the module from the specification
- module = importlib.util.module_from_spec(moduleSpec)
+ module = importlib.util.module_from_spec(module_spec)
logger.debug(f"Module: {module}")
# Load the module
- sys.modules[moduleName] = module
- moduleSpec.loader.exec_module(module)
- logger.info(f"Loaded module {moduleName}")
+ sys.modules[module_name] = module
+ module_spec.loader.exec_module(module)
+ logger.info(f"Loaded module {module_name}")
# Find the Processor class and return it
for name, member in inspect.getmembers(module):
@@ -579,27 +371,27 @@ class ExtensionManager:
return None
- def __is_processor_class(self, potentialProcessorClass):
+ def __is_processor_class(self, potential_processor_class):
# Go through all members of the given class and see if it has an inner
class named Java
- for name, member in inspect.getmembers(potentialProcessorClass):
+ for name, member in inspect.getmembers(potential_processor_class):
if name == 'Java' and inspect.isclass(member):
# Instantiate the Java class
instance = member()
# Check if the instance has a method named 'implements'
- hasImplements = False
+ has_implements = False
for attr in dir(instance):
if attr == 'implements':
- hasImplements = True
+ has_implements = True
break
# If not, move to the next member
- if not hasImplements:
+ if not has_implements:
continue
# The class implements something. Check if it implements
Processor
for interface in instance.implements:
- if interface in self.processorInterfaces:
- logger.debug(f"{potentialProcessorClass} implements
Processor")
+ if interface in self.processor_interfaces:
+ logger.debug(f"{potential_processor_class} implements
Processor")
return True
- return False
\ No newline at end of file
+ return False
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/ProcessorInspection.py
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/ProcessorInspection.py
new file mode 100644
index 0000000000..c01b5a3a52
--- /dev/null
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/ProcessorInspection.py
@@ -0,0 +1,295 @@
+# 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 ast
+import ExtensionDetails
+import logging
+import textwrap
+from nifiapi.documentation import UseCaseDetails,
MultiProcessorUseCaseDetails, ProcessorConfiguration, PropertyDescription
+
+PROCESSOR_INTERFACES = ['org.apache.nifi.python.processor.FlowFileTransform',
'org.apache.nifi.python.processor.RecordTransform']
+
+logger = logging.getLogger("python.ProcessorInspection")
+
+
+def get_processor_class_nodes(module_file: str) -> list:
+ with open(module_file) as file:
+ root_node = ast.parse(file.read())
+
+ processor_class_nodes = []
+ class_nodes = get_class_nodes(root_node)
+ for class_node in class_nodes:
+ if is_processor_class_node(class_node):
+ processor_class_nodes.append(class_node)
+
+ return processor_class_nodes
+
+
+def get_processor_details(class_node, module_file):
+ # Look for a 'ProcessorDetails' class
+ child_class_nodes = get_class_nodes(class_node)
+
+ # Get the Java interfaces that it implements
+ interfaces = get_java_interfaces(class_node)
+
+ for child_class_node in child_class_nodes:
+ if child_class_node.name == 'ProcessorDetails':
+ logger.debug(f"Found ProcessorDetails class in {class_node.name}")
+ version = __get_processor_version(child_class_node)
+ dependencies = __get_processor_dependencies(child_class_node,
class_node.name)
+ description = __get_processor_description(child_class_node)
+ tags = __get_processor_tags(child_class_node)
+ use_cases = get_use_cases(class_node)
+ multi_processor_use_cases =
get_multi_processor_use_cases(class_node)
+ property_descriptions = get_property_descriptions(class_node)
+
+ return ExtensionDetails.ExtensionDetails(interfaces=interfaces,
+ type=class_node.name,
+ version=version,
+ dependencies=dependencies,
+
source_location=module_file,
+ description=description,
+ tags=tags,
+ use_cases=use_cases,
+
multi_processor_use_cases=multi_processor_use_cases,
+
property_descriptions=property_descriptions)
+
+ return ExtensionDetails.ExtensionDetails(interfaces=interfaces,
+ type=class_node.name,
+ version='Unknown',
+ dependencies=[],
+ source_location=module_file)
+
+
+def __get_processor_version(details_node):
+ return get_assigned_value(details_node, 'version', 'Unknown')
+
+
+def __get_processor_dependencies(details_node, class_name):
+ deps = get_assigned_value(details_node, 'dependencies', [])
+ if len(deps) == 0:
+ logger.info("Found no external dependencies that are required for
class %s" % class_name)
+ else:
+ logger.info("Found the following external dependencies that are
required for class {0}: {1}".format(class_name, deps))
+
+ return deps
+
+
+def __get_processor_tags(details_node):
+ return get_assigned_value(details_node, 'tags', [])
+
+
+def get_use_cases(class_node) -> list[UseCaseDetails]:
+ decorators = class_node.decorator_list
+ if not decorators:
+ return []
+
+ use_cases = []
+ for decorator in decorators:
+ if decorator.func.id != 'use_case' or not decorator.keywords:
+ continue
+
+ kv_pairs = {}
+ for keyword in decorator.keywords:
+ keyword_name = keyword.arg
+ keyword_value = get_constant_values(keyword.value)
+ if keyword_value is not None and isinstance(keyword_value, str):
+ keyword_value = textwrap.dedent(keyword_value).strip()
+ kv_pairs[keyword_name] = keyword_value
+
+ use_case = UseCaseDetails(
+ description=dedent(kv_pairs.get('description')),
+ notes=dedent(kv_pairs.get('notes')),
+ keywords=kv_pairs.get('keywords'),
+ configuration=dedent(kv_pairs.get('configuration'))
+ )
+ use_cases.append(use_case)
+
+ return use_cases
+
+def dedent(val: str) -> str:
+ if not val:
+ return ""
+ return textwrap.dedent(val)
+
+def get_multi_processor_use_cases(class_node) ->
list[MultiProcessorUseCaseDetails]:
+ decorators = class_node.decorator_list
+ if not decorators:
+ return []
+
+ use_cases = []
+ for decorator in decorators:
+ if decorator.func.id != 'multi_processor_use_case' or not
decorator.keywords:
+ continue
+
+ kv_pairs = {}
+ for keyword in decorator.keywords:
+ keyword_name = keyword.arg
+ if keyword_name == 'configurations':
+ keyword_value = get_processor_configurations(keyword.value)
+ else:
+ keyword_value = get_constant_values(keyword.value)
+
+ if keyword_value is not None and isinstance(keyword_value, str):
+ keyword_value = textwrap.dedent(keyword_value).strip()
+ kv_pairs[keyword_name] = keyword_value
+
+ use_case = MultiProcessorUseCaseDetails(
+ description=dedent(kv_pairs.get('description')),
+ notes=dedent(kv_pairs.get('notes')),
+ keywords=kv_pairs.get('keywords'),
+ configurations=kv_pairs.get('configurations')
+ )
+ use_cases.append(use_case)
+
+ return use_cases
+
+
+def get_processor_configurations(constructor_calls: ast.List) -> list:
+ configurations = []
+ for constructor_call in constructor_calls.elts:
+ if not isinstance(constructor_call, ast.Call) or
constructor_call.func.id != 'ProcessorConfiguration':
+ continue
+
+ kv_pairs = {}
+ for keyword in constructor_call.keywords:
+ keyword_name = keyword.arg
+ keyword_value = get_constant_values(keyword.value)
+ if keyword_value is not None and isinstance(keyword_value, str):
+ keyword_value = textwrap.dedent(keyword_value).strip()
+
+ kv_pairs[keyword_name] = keyword_value
+
+ processor_config =
ProcessorConfiguration(processor_type=kv_pairs.get('processor_type'),
+
configuration=kv_pairs.get('configuration'))
+ configurations.append(processor_config)
+
+ return configurations
+
+
+def get_property_descriptions(class_node):
+ descriptions = []
+
+ for element in class_node.body:
+ if not isinstance(element, ast.Assign) or not element.value:
+ continue
+ if not isinstance(element.value, ast.Call):
+ continue
+ if element.value.func.id != 'PropertyDescriptor':
+ continue
+ if not element.value.keywords:
+ continue
+
+ descriptor_info = {}
+ for keyword in element.value.keywords:
+ key = keyword.arg
+ value = get_constant_values(keyword.value)
+ descriptor_info[key] = value
+
+ description = PropertyDescription(name=descriptor_info.get('name'),
+
description=descriptor_info.get('description'),
+
display_name=replace_null(descriptor_info.get('display_name'),
descriptor_info.get('name')),
+
required=replace_null(descriptor_info.get('required'), False),
+
sensitive=replace_null(descriptor_info.get('sensitive'), False),
+
default_value=descriptor_info.get('default_value'),
+
expression_language_scope=replace_null(descriptor_info.get('expression_language_scope'),
'NONE'),
+
controller_service_definition=descriptor_info.get('controller_service_definition'))
+ descriptions.append(description)
+
+ return descriptions
+
+
+def replace_null(val: any, replacement: any):
+ return val if val else replacement
+
+def get_assigned_value(class_node, assignment_id, default_value=None):
+ assignments = get_assignment_nodes(class_node)
+ for assignment in assignments:
+ targets = assignment.targets
+ if len(targets) != 1 or targets[0].id != assignment_id:
+ continue
+ assigned_value = assignment.value
+ return get_constant_values(assigned_value)
+
+ return default_value
+
+def get_constant_values(val):
+ if isinstance(val, ast.Constant):
+ return val.value
+ if isinstance(val, ast.List):
+ return [get_constant_values(v) for v in val.elts]
+ if isinstance(val, ast.Dict):
+ keys = val.keys
+ values = val.values
+ key_values = [get_constant_values(v).strip() for v in keys]
+ value_values = [get_constant_values(v).strip() for v in values]
+ return dict(zip(key_values, value_values))
+ if isinstance(val, ast.Attribute):
+ return val.attr
+ if isinstance(val, ast.BinOp) and isinstance(val.op, ast.Add):
+ left = get_constant_values(val.left)
+ right = get_constant_values(val.right)
+ if left and right:
+ return left + right
+ if left and not right:
+ return left
+ if right and not left:
+ return right
+
+ return None
+
+
+def __get_processor_description(details_node):
+ return get_assigned_value(details_node, 'description')
+
+
+def is_processor_class_node(class_node):
+ """
+ Checks if the Abstract Syntax Tree (AST) Node represents a Processor class.
+ We are looking for any classes within the given module file that look like:
+
+ class MyProcessor:
+ ...
+ class Java:
+ implements = ['org.apache.nifi.python.processor.FlowFileTransform']
+
+ :param class_node: the abstract syntax tree (AST) node
+ :return: True if the AST Node represents a Python Class that is a
Processor, False otherwise
+ """
+
+ # Look for a 'Java' sub-class
+ interfaces = get_java_interfaces(class_node)
+ return len(interfaces) > 0
+
+def get_java_interfaces(class_node):
+ # Get all class definition nodes
+ child_class_nodes = get_class_nodes(class_node)
+
+ interfaces = []
+ for child_class_node in child_class_nodes:
+ # Look for a 'Java' sub-class
+ if child_class_node.name != 'Java':
+ continue
+
+ implemented = get_assigned_value(child_class_node, 'implements')
+ interfaces.extend([ifc_name for ifc_name in implemented if ifc_name in
PROCESSOR_INTERFACES])
+
+ return interfaces
+
+def get_class_nodes(node) -> list:
+ return [n for n in node.body if isinstance(n, ast.ClassDef)]
+
+def get_assignment_nodes(node) -> list:
+ return [n for n in node.body if isinstance(n, ast.Assign)]
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/PythonProcessorAdapter.py
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/PythonProcessorAdapter.py
index ea7b88c302..4029a70c5e 100644
---
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/PythonProcessorAdapter.py
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/main/python/framework/PythonProcessorAdapter.py
@@ -16,6 +16,14 @@
from nifiapi.properties import ProcessContext
+def is_method_defined(processor, method_name):
+ # Get the attribute from the given Processor with the provided method
name, returning None if method is not present
+ attr = getattr(processor, method_name, None)
+
+ # Return True if the attribute is present and is a method (i.e., is
callable).
+ return callable(attr)
+
+
# PythonProcessorAdapter is responsible for receiving method invocations from
Java side and delegating to the appropriate
# method for a Processor. We use this adapter instead of calling directly into
the Processor because it allows us to be more
# flexible on the Python side, by allowing us to avoid implementing things
like customValidate, etc.
@@ -26,30 +34,23 @@ class PythonProcessorAdapter:
def __init__(self, gateway, processor, extension_manager,
controller_service_type_lookup):
self.processor = processor
self.gateway = gateway
- self.hasCustomValidate = self.hasMethod(processor, 'customValidate')
+ self.hasCustomValidate = is_method_defined(processor, 'customValidate')
self.extension_manager = extension_manager
self.controller_service_type_lookup = controller_service_type_lookup
- self.has_properties = self.hasMethod(processor,
'getPropertyDescriptors')
- self.supportsDynamicProperties = self.hasMethod(processor,
'getDynamicPropertyDescriptor')
+ self.has_properties = is_method_defined(processor,
'getPropertyDescriptors')
+ self.supportsDynamicProperties = is_method_defined(processor,
'getDynamicPropertyDescriptor')
- if self.hasMethod(processor, 'getRelationships'):
+ if is_method_defined(processor, 'getRelationships'):
self.relationships = None
else:
self.relationships = gateway.jvm.java.util.HashSet()
success =
gateway.jvm.org.apache.nifi.processor.Relationship.Builder() \
.name("success") \
- .description("All FlowFiles will go to this relationship") \
+ .description("All FlowFiles will go to this relationship after
being successfully processed") \
.build()
self.relationships.add(success)
- def hasMethod(self, processor, methodName):
- # Get the attribute from the given Processor with the provided method
name, returning None if method is not present
- attr = getattr(processor, methodName, None)
-
- # Return True if the attribute is present and is a method (i.e., is
callable).
- return callable(attr)
-
def customValidate(self, context):
# If no customValidate method, just return
if not self.hasCustomValidate:
@@ -59,7 +60,7 @@ class PythonProcessorAdapter:
def getRelationships(self):
# If self.relationships is None, it means that the Processor has
implemented the method, and we need
- # to call the Processor's implementation. This allows for dynamically
change the Relationships based on
+ # to call the Processor's implementation. This allows for dynamically
changing the Relationships based on
# configuration, etc.
if self.relationships is None:
return self.processor.getRelationships()
@@ -68,11 +69,11 @@ class PythonProcessorAdapter:
def getSupportedPropertyDescriptors(self):
descriptors = self.processor.getPropertyDescriptors() if
self.has_properties else []
- descriptorList = self.gateway.jvm.java.util.ArrayList()
+ descriptor_list = self.gateway.jvm.java.util.ArrayList()
for descriptor in descriptors:
- descriptorList.add(descriptor.to_java_descriptor(self.gateway,
self.controller_service_type_lookup))
+ descriptor_list.add(descriptor.to_java_descriptor(self.gateway,
self.controller_service_type_lookup))
- return descriptorList
+ return descriptor_list
def getProcessor(self):
return self.processor
@@ -80,18 +81,18 @@ class PythonProcessorAdapter:
def isDynamicPropertySupported(self):
return self.supportsDynamicProperties
- def getSupportedDynamicPropertyDescriptor(self, propertyName):
+ def getSupportedDynamicPropertyDescriptor(self, property_name):
if not self.supportsDynamicProperties:
return None
- descriptor = self.processor.getDynamicPropertyDescriptor(propertyName)
- return None if descriptor is None else
descriptor.to_java_descriptor(gateway = self.gateway,
cs_type_lookup=self.controller_service_type_lookup)
+ descriptor = self.processor.getDynamicPropertyDescriptor(property_name)
+ return None if descriptor is None else
descriptor.to_java_descriptor(gateway=self.gateway,
cs_type_lookup=self.controller_service_type_lookup)
def onScheduled(self, context):
- if self.hasMethod(self.processor, 'onScheduled'):
+ if is_method_defined(self.processor, 'onScheduled'):
self.processor.onScheduled(ProcessContext(context))
def onStopped(self, context):
- if self.hasMethod(self.processor, 'onStopped'):
+ if is_method_defined(self.processor, 'onStopped'):
self.processor.onStopped(ProcessContext(context))
def initialize(self, context):
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/test/python/framework/DummyProcessor.py
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/test/python/framework/DummyProcessor.py
new file mode 100644
index 0000000000..7bcc74da94
--- /dev/null
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/test/python/framework/DummyProcessor.py
@@ -0,0 +1,45 @@
+# 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.documentation import use_case, multi_processor_use_case,
ProcessorConfiguration
+
+@use_case(description="First Use Case",
+ notes="First Note",
+ keywords=["A", "B"],
+ configuration="""This Processor has no configuration.""")
+@use_case(description="Second Use Case",
+ notes="Another Note",
+ keywords=["C", "B"],
+ configuration="""No config.""")
+@multi_processor_use_case(description="Multi Processor Use Case",
+ notes="Note #1",
+ keywords=["D", "E"],
+ configurations=[
+ ProcessorConfiguration(processor_type="OtherProcessor",
+ configuration="No config "
+ "necessary."),
+ ProcessorConfiguration(processor_type="DummyProcessor",
+ configuration="None.")
+ ])
+class DummyProcessor:
+ class Java:
+ implements = ['org.apache.nifi.python.processor.FlowFileTransform']
+
+ class ProcessorDetails:
+ description = "Fake Processor"
+ tags = ["tag1", "tag2"]
+
+ def __init__(self):
+ pass
diff --git
a/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/test/python/framework/TestPythonProcessorAdapter.py
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/test/python/framework/TestPythonProcessorAdapter.py
new file mode 100644
index 0000000000..c1075d6078
--- /dev/null
+++
b/nifi-nar-bundles/nifi-py4j-bundle/nifi-python-framework/src/test/python/framework/TestPythonProcessorAdapter.py
@@ -0,0 +1,53 @@
+# 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 unittest
+import ProcessorInspection
+
+DUMMY_PROCESSOR_FILE = 'src/test/python/framework/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)
+ self.assertIsNotNone(details)
+ self.assertEqual(details.description, 'Fake Processor')
+ self.assertEqual(details.tags, ['tag1', 'tag2'])
+ self.assertEqual(len(details.use_cases), 2)
+ self.assertEqual(details.use_cases[0].description, 'First Use Case')
+ self.assertEqual(details.use_cases[1].description, 'Second Use Case')
+ self.assertEqual(details.use_cases[0].notes, 'First Note')
+ self.assertEqual(details.use_cases[1].notes, 'Another Note')
+ self.assertEqual(details.use_cases[0].configuration, 'This Processor
has no configuration.')
+ self.assertEqual(details.use_cases[0].keywords, ['A', 'B'])
+
+ self.assertEqual(len(details.multi_processor_use_cases), 1)
+ self.assertEqual(details.multi_processor_use_cases[0].description,
'Multi Processor Use Case')
+ self.assertEqual(details.multi_processor_use_cases[0].notes, 'Note #1')
+ self.assertEqual(details.multi_processor_use_cases[0].keywords, ['D',
'E'])
+
self.assertEqual(len(details.multi_processor_use_cases[0].configurations), 2)
+
self.assertEqual(details.multi_processor_use_cases[0].configurations[0].processor_type,
'OtherProcessor')
+
self.assertEqual(details.multi_processor_use_cases[0].configurations[0].configuration,
'No config necessary.')
+
self.assertEqual(details.multi_processor_use_cases[0].configurations[1].processor_type,
'DummyProcessor')
+
self.assertEqual(details.multi_processor_use_cases[0].configurations[1].configuration,
'None.')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git
a/nifi-python-extensions/nifi-openai-module/src/main/python/PromptChatGPT.py
b/nifi-python-extensions/nifi-openai-module/src/main/python/PromptChatGPT.py
index 62528f95ee..5b8e3738fe 100644
--- a/nifi-python-extensions/nifi-openai-module/src/main/python/PromptChatGPT.py
+++ b/nifi-python-extensions/nifi-openai-module/src/main/python/PromptChatGPT.py
@@ -47,7 +47,7 @@ class PromptChatGPT(FlowFileTransform):
PROMPT = PropertyDescriptor(
name="Prompt",
description="The prompt to issue to ChatGPT. This may use FlowFile
attributes via Expression Language and may also reference the FlowFile content
by using the literal " +
- f"{FLOWFILE_CONTENT_REFERENCE} (including braces) in the
prompt. If the FlowFile's content is JSON formatted, a reference may also
include JSONPath Expressions "
+ "{flowfile_content} (including braces) in the prompt. If
the FlowFile's content is JSON formatted, a reference may also include JSONPath
Expressions "
"to reference specific fields in the FlowFile content,
such as {$.page_content}",
validators=[StandardValidators.NON_EMPTY_VALIDATOR],
expression_language_scope=ExpressionLanguageScope.FLOWFILE_ATTRIBUTES,
diff --git
a/nifi-python-extensions/nifi-text-embeddings-module/src/main/python/ChunkDocument.py
b/nifi-python-extensions/nifi-text-embeddings-module/src/main/python/ChunkDocument.py
index cfe0c185b3..9b6f38e69f 100644
---
a/nifi-python-extensions/nifi-text-embeddings-module/src/main/python/ChunkDocument.py
+++
b/nifi-python-extensions/nifi-text-embeddings-module/src/main/python/ChunkDocument.py
@@ -18,6 +18,8 @@ import json
from langchain.text_splitter import Language
from nifiapi.flowfiletransform import FlowFileTransform,
FlowFileTransformResult
from nifiapi.properties import PropertyDescriptor, StandardValidators,
PropertyDependency, ExpressionLanguageScope
+from nifiapi.documentation import use_case, multi_processor_use_case,
ProcessorConfiguration
+
SPLIT_BY_CHARACTER = 'Split by Character'
SPLIT_CODE = 'Split Code'
@@ -26,14 +28,86 @@ RECURSIVELY_SPLIT_BY_CHARACTER = 'Recursively Split by
Character'
TEXT_KEY = "text"
METADATA_KEY = "metadata"
+@use_case(
+ description="Create chunks of text from a single larger chunk.",
+ notes="The input for this use case is expected to be a FlowFile whose
content is a JSON Lines document, with each line having a 'text' and a
'metadata' element.",
+ keywords=["embedding", "vector", "text", "rag", "retrieval augmented
generation"],
+ configuration="""
+ Set "Input Format" to "Plain Text"
+ Set "Element Strategy" to "Single Document"
+ """
+)
+@multi_processor_use_case(
+ description="""
+ Chunk Plaintext data in order to prepare it for storage in a vector
store. The output is in "json-lines" format,
+ containing the chunked data as text, as well as metadata pertaining to
the chunk.""",
+ notes="The input for this use case is expected to be a FlowFile whose
content is a plaintext document.",
+ keywords=["embedding", "vector", "text", "rag", "retrieval augmented
generation"],
+ configurations=[
+ ProcessorConfiguration(
+ processor_type="ParseDocument",
+ configuration="""
+ Set "Input Format" to "Plain Text"
+ Set "Element Strategy" to "Single Document"
+
+ Connect the 'success' Relationship to ChunkDocument.
+ """
+ ),
+ ProcessorConfiguration(
+ processor_type="ChunkDocument",
+ configuration="""
+ Set the following properties:
+ "Chunking Strategy" = "Recursively Split by Character"
+ "Separator" = "\\n\\n,\\n, ,"
+ "Separator Format" = "Plain Text"
+ "Chunk Size" = "4000"
+ "Chunk Overlap" = "200"
+ "Keep Separator" = "false"
+
+ Connect the 'success' Relationship to the appropriate
destination to store data in the desired vector store.
+ """
+ )
+ ])
+@multi_processor_use_case(
+ description="""
+ Parse and chunk the textual contents of a PDF document in order to
prepare it for storage in a vector store. The output is in "json-lines" format,
+ containing the chunked data as text, as well as metadata pertaining to
the chunk.""",
+ notes="The input for this use case is expected to be a FlowFile whose
content is a PDF document.",
+ keywords=["pdf", "embedding", "vector", "text", "rag", "retrieval
augmented generation"],
+ configurations=[
+ ProcessorConfiguration(
+ processor_type="ParseDocument",
+ configuration="""
+ Set "Input Format" to "PDF"
+ Set "Element Strategy" to "Single Document"
+ Set "Include Extracted Metadata" to "false"
+
+ Connect the 'success' Relationship to ChunkDocument.
+ """
+ ),
+ ProcessorConfiguration(
+ processor_type="ChunkDocument",
+ configuration="""
+ Set the following properties:
+ "Chunking Strategy" = "Recursively Split by Character"
+ "Separator" = "\\n\\n,\\n, ,"
+ "Separator Format" = "Plain Text"
+ "Chunk Size" = "4000"
+ "Chunk Overlap" = "200"
+ "Keep Separator" = "false"
+ Connect the 'success' Relationship to the appropriate
destination to store data in the desired vector store.
+ """
+ )
+ ])
class ChunkDocument(FlowFileTransform):
class Java:
implements = ['org.apache.nifi.python.processor.FlowFileTransform']
class ProcessorDetails:
version = '2.0.0-SNAPSHOT'
- description = """Splits incoming documents into chunks that are
appropriately sized for creating Text Embeddings. The input is expected to be
in "json-lines" format, with
- each line having a 'text' and a 'metadata' element. Each line will
then be split into one or more lines in the output."""
+ description = """Chunks incoming documents that are formatted as JSON
Lines into chunks that are appropriately sized for creating Text Embeddings.
+ The input is expected to be in "json-lines" format, with each line
having a 'text' and a 'metadata' element.
+ Each line will then be split into one or more lines in the
output."""
tags = ["text", "split", "chunk", "langchain", "embeddings", "vector",
"machine learning", "ML", "artificial intelligence", "ai", "document"]
dependencies = ['langchain']
diff --git
a/nifi-python-extensions/nifi-text-embeddings-module/src/main/python/vectorstores/PutPinecone.py
b/nifi-python-extensions/nifi-text-embeddings-module/src/main/python/vectorstores/PutPinecone.py
index 409f2b9279..67abfa6b09 100644
---
a/nifi-python-extensions/nifi-text-embeddings-module/src/main/python/vectorstores/PutPinecone.py
+++
b/nifi-python-extensions/nifi-text-embeddings-module/src/main/python/vectorstores/PutPinecone.py
@@ -19,8 +19,34 @@ from nifiapi.properties import PropertyDescriptor,
StandardValidators, Expressio
import pinecone
import json
from EmbeddingUtils import OPENAI, HUGGING_FACE, EMBEDDING_MODEL,
create_embedding_service
-
-
+from nifiapi.documentation import use_case, multi_processor_use_case,
ProcessorConfiguration
+
+@use_case(description="Create vectors/embeddings that represent text content
and send the vectors to Pinecone",
+ notes="This use case assumes that the data has already been
formatted in JSONL format with the text to store in Pinecone provided in the
'text' field.",
+ keywords=["pinecone", "embedding", "vector", "text", "vectorstore",
"insert"],
+ configuration="""
+ Configure the 'Pinecone API Key' to the appropriate
authentication token for interacting with Pinecone.
+ Configure 'Embedding Model' to indicate whether OpenAI
embeddings should be used or a HuggingFace embedding model should be used:
'Hugging Face Model' or 'OpenAI Model'
+ Configure the 'OpenAI API Key' or 'HuggingFace API Key',
depending on the chosen Embedding Model.
+ Set 'Pinecone Environment' to the name of your Pinecone
environment
+ Set 'Index Name' to the name of your Pinecone Index.
+ Set 'Namespace' to appropriate namespace, or leave it empty to
use the default Namespace.
+
+ If the documents to send to Pinecone contain a unique
identifier, set the 'Document ID Field Name' property to the name of the field
that contains the document ID.
+ This property can be left blank, in which case a unique ID
will be generated based on the FlowFile's filename.
+ """)
+@use_case(description="Update vectors/embeddings in Pinecone",
+ notes="This use case assumes that the data has already been
formatted in JSONL format with the text to store in Pinecone provided in the
'text' field.",
+ keywords=["pinecone", "embedding", "vector", "text", "vectorstore",
"update", "upsert"],
+ configuration="""
+ Configure the 'Pinecone API Key' to the appropriate
authentication token for interacting with Pinecone.
+ Configure 'Embedding Model' to indicate whether OpenAI
embeddings should be used or a HuggingFace embedding model should be used:
'Hugging Face Model' or 'OpenAI Model'
+ Configure the 'OpenAI API Key' or 'HuggingFace API Key',
depending on the chosen Embedding Model.
+ Set 'Pinecone Environment' to the name of your Pinecone
environment
+ Set 'Index Name' to the name of your Pinecone Index.
+ Set 'Namespace' to appropriate namespace, or leave it empty to
use the default Namespace.
+ Set the 'Document ID Field Name' property to the name of the
field that contains the identifier of the document in Pinecone to update.
+ """)
class PutPinecone(FlowFileTransform):
class Java:
implements = ['org.apache.nifi.python.processor.FlowFileTransform']
@@ -31,6 +57,7 @@ class PutPinecone(FlowFileTransform):
The text must be a string, while metadata must be a map
with strings for values. Any additional fields will be ignored."""
tags = ["pinecone", "vector", "vectordb", "vectorstore", "embeddings",
"ai", "artificial intelligence", "ml", "machine learning", "text", "LLM"]
+
PINECONE_API_KEY = PropertyDescriptor(
name="Pinecone API Key",
description="The API Key to use in order to authentication with
Pinecone",