diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs
index 917ceb429..e2b0ba7df 100644
--- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs
+++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs
@@ -162,10 +162,14 @@ services
engine.Execute("function sayHelloWorld() { return greet('World'); }");
});
})
- .UsePython(options =>
+ .UsePython(python =>
{
- options.AddScript("def greet(name): return f\"Hello {name}!\";");
- options.AddScript("def say_hello_world(): return greet(\"World\");");
+ python.PythonOptions += options =>
+ {
+ // Make sure to configure the path to the python DLL. E.g. /opt/homebrew/Cellar/python@3.11/3.11.6_1/Frameworks/Python.framework/Versions/3.11/bin/python3.11
+ // alternatively, you can set the PYTHONNET_PYDLL environment variable.
+ configuration.GetSection("Scripting:Python").Bind(options);
+ };
})
.UseLiquid(liquid => liquid.FluidOptions = options => options.Encoder = HtmlEncoder.Default)
.UseHttp(http =>
@@ -192,7 +196,7 @@ services
{
elsa.UseQuartz(quartz => { quartz.UseSqlite(sqliteConnectionString); });
}
-
+
elsa.UseMassTransit(massTransit =>
{
if (useMassTransitAzureServiceBus)
diff --git a/src/bundles/Elsa.WorkflowServer.Web/appsettings.json b/src/bundles/Elsa.WorkflowServer.Web/appsettings.json
index edcfc36f2..6597915a8 100644
--- a/src/bundles/Elsa.WorkflowServer.Web/appsettings.json
+++ b/src/bundles/Elsa.WorkflowServer.Web/appsettings.json
@@ -99,5 +99,14 @@
"SweepInterval": "00:00:10:00",
"BatchSize": 1000
}
+ },
+ "Scripting": {
+ "Python": {
+ "PythonDllPath": "",
+ "Scripts": [
+ "def greet(name): return f'Hello {name}!'",
+ "def say_hello_world(): return greet('World')"
+ ]
+ }
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Python/Elsa.Python.csproj b/src/modules/Elsa.Python/Elsa.Python.csproj
index d0d1c7175..8e0a0b0ae 100644
--- a/src/modules/Elsa.Python/Elsa.Python.csproj
+++ b/src/modules/Elsa.Python/Elsa.Python.csproj
@@ -1,7 +1,7 @@
-
-
+
+ net6.0;net7.0
@@ -12,12 +12,12 @@
-
-
+
+
-
+
diff --git a/src/modules/Elsa.Python/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Python/Extensions/ModuleExtensions.cs
index cf82cec11..4f7c61794 100644
--- a/src/modules/Elsa.Python/Extensions/ModuleExtensions.cs
+++ b/src/modules/Elsa.Python/Extensions/ModuleExtensions.cs
@@ -1,6 +1,5 @@
using Elsa.Features.Services;
using Elsa.Python.Features;
-using Elsa.Python.Options;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
@@ -18,12 +17,4 @@ public static class ModuleExtensions
module.Configure(configure);
return module;
}
-
- ///
- /// Setup the feature.
- ///
- public static IModule UsePython(this IModule module, Action configureOptions)
- {
- return module.UsePython(python => python.PythonOptions += configureOptions);
- }
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Python/Features/PythonFeature.cs b/src/modules/Elsa.Python/Features/PythonFeature.cs
index 29210010c..cdbe5ab8b 100644
--- a/src/modules/Elsa.Python/Features/PythonFeature.cs
+++ b/src/modules/Elsa.Python/Features/PythonFeature.cs
@@ -1,12 +1,11 @@
using Elsa.Common.Features;
-using Elsa.Expressions.Contracts;
using Elsa.Expressions.Features;
using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Python.Contracts;
-using Elsa.Python.Expressions;
+using Elsa.Python.HostedServices;
using Elsa.Python.Options;
using Elsa.Python.Providers;
using Elsa.Python.Services;
@@ -25,26 +24,32 @@ public class PythonFeature : FeatureBase
public PythonFeature(IModule module) : base(module)
{
}
-
+
///
/// Configures the .
///
public Action PythonOptions { get; set; } = _ => { };
-
+
+ ///
+ public override void ConfigureHostedServices()
+ {
+ Module.ConfigureHostedService();
+ }
+
///
public override void Apply()
{
Services.Configure(PythonOptions);
-
- // C# services.
+
+ // Python services.
Services
- .AddSingleton()
+ .AddSingleton()
.AddExpressionDescriptorProvider()
;
// Handlers.
Services.AddNotificationHandlersFrom();
-
+
// Activities.
Module.AddActivitiesFrom();
}
diff --git a/src/modules/Elsa.Python/Handlers/AddInputAccessors.cs b/src/modules/Elsa.Python/Handlers/AddInputAccessors.cs
index 62b544c07..b69f9f18e 100644
--- a/src/modules/Elsa.Python/Handlers/AddInputAccessors.cs
+++ b/src/modules/Elsa.Python/Handlers/AddInputAccessors.cs
@@ -14,9 +14,9 @@ public class AddInputAccessors : INotificationHandler
///
public Task HandleAsync(EvaluatingPython notification, CancellationToken cancellationToken)
{
- var scope = notification.ScriptScope;
+ var scope = notification.Scope;
var inputProxy = new InputProxy(notification.Context);
- scope.SetVariable("input", inputProxy);
+ scope.Set("input", inputProxy);
return Task.CompletedTask;
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Python/Handlers/ConfigurePythonFromOptions.cs b/src/modules/Elsa.Python/Handlers/ConfigurePythonFromOptions.cs
index 4dfcabb6b..dcbfaea70 100644
--- a/src/modules/Elsa.Python/Handlers/ConfigurePythonFromOptions.cs
+++ b/src/modules/Elsa.Python/Handlers/ConfigurePythonFromOptions.cs
@@ -25,8 +25,8 @@ public class ConfigurePythonFromOptions : INotificationHandler
///
public Task HandleAsync(EvaluatingPython notification, CancellationToken cancellationToken)
{
- foreach (var script in _options.Scripts)
- notification.AppendScript(script);
+ foreach (var action in _options.Scopes) action(notification.Scope);
+ foreach (var script in _options.Scripts) notification.AppendScript(script);
return Task.CompletedTask;
}
diff --git a/src/modules/Elsa.Python/Handlers/GenerateWorkflowVariableAccessors.cs b/src/modules/Elsa.Python/Handlers/GenerateWorkflowVariableAccessors.cs
index 9d4f5cacb..4301a7814 100644
--- a/src/modules/Elsa.Python/Handlers/GenerateWorkflowVariableAccessors.cs
+++ b/src/modules/Elsa.Python/Handlers/GenerateWorkflowVariableAccessors.cs
@@ -31,14 +31,14 @@ public class GenerateWorkflowVariableAccessors : INotificationHandler
+/// Initializes the Python engine.
+///
+public class PythonGlobalInterpreterManager : IHostedService
+{
+ private readonly IOptions _options;
+ private IntPtr _mainThreadState;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PythonGlobalInterpreterManager(IOptions options)
+ {
+ _options = options;
+ }
+
+ ///
+ public Task StartAsync(CancellationToken cancellationToken)
+ {
+ if (!string.IsNullOrEmpty(_options.Value.PythonDllPath))
+ Environment.SetEnvironmentVariable("PYTHONNET_PYDLL", _options.Value.PythonDllPath);
+
+ PythonEngine.Initialize();
+ _mainThreadState = PythonEngine.BeginAllowThreads();
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ PythonEngine.EndAllowThreads(_mainThreadState);
+ PythonEngine.Shutdown();
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Elsa.Python/Models/ExecutionContextProxy.cs b/src/modules/Elsa.Python/Models/ExecutionContextProxy.cs
index f36360222..eedea4b66 100644
--- a/src/modules/Elsa.Python/Models/ExecutionContextProxy.cs
+++ b/src/modules/Elsa.Python/Models/ExecutionContextProxy.cs
@@ -27,22 +27,22 @@ public partial class ExecutionContextProxy
///
/// Gets the value of the specified variable.
///
- public object? get_variable(Type type, string name) => ExpressionExecutionContext.GetVariableInScope(name).ConvertTo(type);
+ public object? GetVariable(string name) => ExpressionExecutionContext.GetVariableInScope(name).ConvertTo();
///
/// Sets the value of the specified variable.
///
- public void set_variable(string name, object? value) => ExpressionExecutionContext.SetVariable(name, value);
+ public void SetVariable(string name, object? value) => ExpressionExecutionContext.SetVariable(name, value);
///
/// Gets the workflow instance ID.
///
- public string workflow_instance_id => ExpressionExecutionContext.GetWorkflowExecutionContext().Id;
+ public string WorkflowInstanceId => ExpressionExecutionContext.GetWorkflowExecutionContext().Id;
///
/// Gets or sets the correlation ID.
///
- public string? correlation_id
+ public string? CorrelationId
{
get => ExpressionExecutionContext.GetWorkflowExecutionContext().CorrelationId;
set => ExpressionExecutionContext.GetWorkflowExecutionContext().CorrelationId = value;
diff --git a/src/modules/Elsa.Python/Models/InputProxy.cs b/src/modules/Elsa.Python/Models/InputProxy.cs
index 6f51a9cfb..4a6f58ce6 100644
--- a/src/modules/Elsa.Python/Models/InputProxy.cs
+++ b/src/modules/Elsa.Python/Models/InputProxy.cs
@@ -26,10 +26,10 @@ public class InputProxy
///
/// Gets the value of the specified input.
///
- public object? get(string name) => Context.GetInput(name);
+ public object? Get(string name) => Context.GetInput(name);
///
/// Gets the value of the specified input.
///
- public object? get(Type type, string name) => Context.GetInput(name).ConvertTo(type);
+ public object? Get(Type type, string name) => Context.GetInput(name).ConvertTo(type);
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Python/Models/OutcomeProxy.cs b/src/modules/Elsa.Python/Models/OutcomeProxy.cs
index 3f7859c0b..77c1b0e2e 100644
--- a/src/modules/Elsa.Python/Models/OutcomeProxy.cs
+++ b/src/modules/Elsa.Python/Models/OutcomeProxy.cs
@@ -27,7 +27,7 @@ public class OutcomeProxy
/// Sets the outcome of the current activity.
///
/// The names of the outcomes.
- public void set(params string[] outcomeNames)
+ public void Set(params string[] outcomeNames)
{
ExpressionExecutionContext.TransientProperties[OutcomePropertiesKey] = outcomeNames;
}
diff --git a/src/modules/Elsa.Python/Models/OutputProxy.cs b/src/modules/Elsa.Python/Models/OutputProxy.cs
index dcc086552..c0b73544e 100644
--- a/src/modules/Elsa.Python/Models/OutputProxy.cs
+++ b/src/modules/Elsa.Python/Models/OutputProxy.cs
@@ -29,7 +29,7 @@ public class OutputProxy
/// The ID or name of the activity that produced the output.
/// The name of the output.
/// The value of the output.
- public object? get(string activityIdOrName, string? outputName = default) => Context.GetOutput(activityIdOrName, outputName);
+ public object? Get(string activityIdOrName, string? outputName = default) => Context.GetOutput(activityIdOrName, outputName);
///
/// Gets the value of the specified output.
@@ -38,10 +38,10 @@ public class OutputProxy
/// The ID or name of the activity that produced the output.
/// The name of the output.
/// The value of the output.
- public object? get(Type returnType, string activityIdOrName, string? outputName = default) => get(activityIdOrName, outputName).ConvertTo(returnType);
+ public object? Get(Type returnType, string activityIdOrName, string? outputName = default) => Get(activityIdOrName, outputName).ConvertTo(returnType);
///
/// Gets the result of the last activity that executed.
///
- public object? last_result => Context.GetLastResult();
+ public object? LastResult => Context.GetLastResult();
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Python/Notifications/EvaluatingPython.cs b/src/modules/Elsa.Python/Notifications/EvaluatingPython.cs
index 3dcb4adcd..da9804b2a 100644
--- a/src/modules/Elsa.Python/Notifications/EvaluatingPython.cs
+++ b/src/modules/Elsa.Python/Notifications/EvaluatingPython.cs
@@ -1,14 +1,14 @@
using System.Text;
using Elsa.Expressions.Models;
using Elsa.Mediator.Contracts;
-using Microsoft.Scripting.Hosting;
+using Python.Runtime;
namespace Elsa.Python.Notifications;
///
/// This notification is published every time a Python expression is about to be evaluated, giving subscribers a chance to modify the Python engine.
///
-public record EvaluatingPython(ScriptEngine Engine, ScriptScope ScriptScope, ExpressionExecutionContext Context) : INotification
+public record EvaluatingPython(PyModule Scope, ExpressionExecutionContext Context) : INotification
{
///
/// Appends a script to the Python engine.
@@ -27,7 +27,6 @@ public record EvaluatingPython(ScriptEngine Engine, ScriptScope ScriptScope, Exp
/// The script to append.
public void AppendScript(string script)
{
- var engine = Engine;
- engine.Execute(script, ScriptScope);
+ Scope.Exec(script);
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Python/Options/PythonOptions.cs b/src/modules/Elsa.Python/Options/PythonOptions.cs
index 1d5d79750..cbd7c9255 100644
--- a/src/modules/Elsa.Python/Options/PythonOptions.cs
+++ b/src/modules/Elsa.Python/Options/PythonOptions.cs
@@ -1,13 +1,20 @@
using System.Text;
-using Microsoft.Scripting.Hosting;
+using JetBrains.Annotations;
+using Python.Runtime;
namespace Elsa.Python.Options;
///
/// Options for the Python expression evaluator.
///
+[PublicAPI]
public class PythonOptions
{
+ ///
+ /// Gets or sets the path to the Python DLL. Alternatively, you can set the PYTHON_DLL environment variable, which is required if you leave this property empty.
+ ///
+ public string? PythonDllPath { get; set; }
+
///
/// Gets or sets the Python script files to load.
///
@@ -16,7 +23,7 @@ public class PythonOptions
///
/// Gets or sets a list of callbacks that are invoked when the Python engine is being configured.
///
- public ICollection> ScriptScopes { get; } = new List>();
+ public ICollection> Scopes { get; } = new List>();
///
/// Appends a script to the Python engine.
@@ -41,8 +48,8 @@ public class PythonOptions
///
/// Registers a callback that is invoked when the Python engine is being configured.
///
- public void ConfigureScriptScope(Action configure)
+ public void ConfigureScriptScope(Action configure)
{
- ScriptScopes.Add(configure);
+ Scopes.Add(configure);
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Python/Services/IronPythonEvaluator.cs b/src/modules/Elsa.Python/Services/IronPythonEvaluator.cs
deleted file mode 100644
index 81996bd1d..000000000
--- a/src/modules/Elsa.Python/Services/IronPythonEvaluator.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using Elsa.Expressions.Helpers;
-using Elsa.Expressions.Models;
-using Elsa.Mediator.Contracts;
-using Elsa.Python.Contracts;
-using Elsa.Python.Models;
-using Elsa.Python.Notifications;
-
-namespace Elsa.Python.Services;
-
-///
-/// Evaluates Python expressions using IronPython.
-///
-public class IronPythonEvaluator : IPythonEvaluator
-{
- private readonly INotificationSender _notificationSender;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public IronPythonEvaluator(INotificationSender notificationSender)
- {
- _notificationSender = notificationSender;
- }
-
- ///
- public async Task