From b5de556a383653ecb30ab75e193fadbd0970f522 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 6 Nov 2025 07:51:34 +0000
Subject: [PATCH 1/8] Initial plan
From 061bdaa4e530a5b5755cbd176d5da3487670799c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 6 Nov 2025 08:01:59 +0000
Subject: [PATCH 2/8] Add Streamlit support to Python Extensions
Co-authored-by: tommasodotNET <12819039+tommasodotNET@users.noreply.github.com>
---
.../Program.cs | 3 +
examples/python/streamlit-api/.gitignore | 3 +
examples/python/streamlit-api/.python-version | 1 +
examples/python/streamlit-api/app.py | 18 +++
examples/python/streamlit-api/pyproject.toml | 12 ++
.../README.md | 10 ++
.../StreamlitAppHostingExtension.cs | 107 ++++++++++++++++++
.../StreamlitAppResource.cs | 12 ++
.../AppHostTests.cs | 1 +
.../ResourceCreationTests.cs | 19 ++++
10 files changed, 186 insertions(+)
create mode 100644 examples/python/streamlit-api/.gitignore
create mode 100644 examples/python/streamlit-api/.python-version
create mode 100644 examples/python/streamlit-api/app.py
create mode 100644 examples/python/streamlit-api/pyproject.toml
create mode 100644 src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
create mode 100644 src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs
diff --git a/examples/python/CommunityToolkit.Aspire.Hosting.Python.Extensions.AppHost/Program.cs b/examples/python/CommunityToolkit.Aspire.Hosting.Python.Extensions.AppHost/Program.cs
index 2188bca08..fa8412642 100644
--- a/examples/python/CommunityToolkit.Aspire.Hosting.Python.Extensions.AppHost/Program.cs
+++ b/examples/python/CommunityToolkit.Aspire.Hosting.Python.Extensions.AppHost/Program.cs
@@ -6,4 +6,7 @@
builder.AddUvApp("uvapp", "../uv-api", "uv-api")
.WithHttpEndpoint(env: "PORT");
+builder.AddStreamlitApp("streamlitapp", "../streamlit-api", "app.py")
+ .WithHttpEndpoint(env: "PORT");
+
builder.Build().Run();
\ No newline at end of file
diff --git a/examples/python/streamlit-api/.gitignore b/examples/python/streamlit-api/.gitignore
new file mode 100644
index 000000000..77ac75498
--- /dev/null
+++ b/examples/python/streamlit-api/.gitignore
@@ -0,0 +1,3 @@
+.venv/
+__pycache__/
+*.pyc
diff --git a/examples/python/streamlit-api/.python-version b/examples/python/streamlit-api/.python-version
new file mode 100644
index 000000000..e4fba2183
--- /dev/null
+++ b/examples/python/streamlit-api/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/examples/python/streamlit-api/app.py b/examples/python/streamlit-api/app.py
new file mode 100644
index 000000000..07450f396
--- /dev/null
+++ b/examples/python/streamlit-api/app.py
@@ -0,0 +1,18 @@
+import streamlit as st
+import os
+
+st.title("Hello, Aspire!")
+
+st.write("This is a simple Streamlit app running in .NET Aspire.")
+
+port = os.environ.get("PORT", "8501")
+st.write(f"Running on port: {port}")
+
+# Add a simple counter
+if "counter" not in st.session_state:
+ st.session_state.counter = 0
+
+if st.button("Click me!"):
+ st.session_state.counter += 1
+
+st.write(f"Button clicked {st.session_state.counter} times")
diff --git a/examples/python/streamlit-api/pyproject.toml b/examples/python/streamlit-api/pyproject.toml
new file mode 100644
index 000000000..390870e1f
--- /dev/null
+++ b/examples/python/streamlit-api/pyproject.toml
@@ -0,0 +1,12 @@
+[project]
+name = "streamlit-api"
+version = "0.1.0"
+description = "Test project for streamlit-api"
+requires-python = ">=3.12"
+dependencies = [
+ "streamlit>=1.40.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md
index 7f15402c3..c1ff5c440 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md
+++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md
@@ -3,6 +3,7 @@
Provides extensions methods and resource definitions for the .NET Aspire AppHost to extend the support for Python applications. Current support includes:
- Uvicorn
- Uv
+- Streamlit
## Getting Started
@@ -36,6 +37,15 @@ var uvicorn = builder.AddUvApp("uvapp", "../uv-api", "uv-api")
.WithHttpEndpoint(env: "PORT");
```
+### Streamlit example usage
+
+Then, in the _Program.cs_ file of `AddStreamlitApp`, define a Streamlit resource, then call `Add`:
+
+```csharp
+var streamlit = builder.AddStreamlitApp("streamlitapp", "../streamlit-api", "app.py")
+ .WithHttpEndpoint(env: "PORT");
+```
+
## Additional Information
https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-python-extensions
diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
new file mode 100644
index 000000000..1eb781491
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
@@ -0,0 +1,107 @@
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Python;
+using CommunityToolkit.Aspire.Utils;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding Streamlit applications to an .
+///
+public static class StreamlitAppHostingExtension
+{
+ ///
+ /// Adds a Streamlit application to the distributed application builder.
+ ///
+ /// The distributed application builder.
+ /// The name of the Streamlit application.
+ /// The directory of the project containing the Streamlit application.
+ /// The path to the Python script to be run by Streamlit.
+ /// Optional arguments to pass to the Streamlit command.
+ /// An for the Streamlit application resource.
+ /// Thrown if is null.
+ /// Thrown if or is null or empty.
+ public static IResourceBuilder AddStreamlitApp(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ string projectDirectory,
+ string scriptPath,
+ params string[] args)
+ {
+ return builder.AddStreamlitApp(name, projectDirectory, scriptPath, ".venv", args);
+ }
+
+ private static IResourceBuilder AddStreamlitApp(
+ this IDistributedApplicationBuilder builder,
+ string name,
+ string projectDirectory,
+ string scriptPath,
+ string virtualEnvironmentPath,
+ params string[] args)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ ArgumentException.ThrowIfNullOrWhiteSpace(scriptPath);
+
+ string wd = projectDirectory ?? Path.Combine("..", name);
+
+ projectDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, wd));
+
+ var virtualEnvironment = new VirtualEnvironment(Path.IsPathRooted(virtualEnvironmentPath)
+ ? virtualEnvironmentPath
+ : Path.Join(projectDirectory, virtualEnvironmentPath));
+
+ var instrumentationExecutable = virtualEnvironment.GetExecutable("opentelemetry-instrument");
+ var streamlitExecutable = virtualEnvironment.GetExecutable("streamlit") ?? "streamlit";
+ var projectExecutable = instrumentationExecutable ?? streamlitExecutable;
+
+ var projectResource = new StreamlitAppResource(name, projectExecutable, projectDirectory);
+
+ var resourceBuilder = builder.AddResource(projectResource).WithArgs(context =>
+ {
+ // If the project is to be automatically instrumented, add the instrumentation executable arguments first.
+ if (!string.IsNullOrEmpty(instrumentationExecutable))
+ {
+ AddOpenTelemetryArguments(context);
+
+ // Add the streamlit executable as the next argument so we can run the project.
+ context.Args.Add("streamlit");
+ }
+
+ AddProjectArguments(scriptPath, args, context);
+ });
+
+ if (!string.IsNullOrEmpty(instrumentationExecutable))
+ {
+ resourceBuilder.WithOtlpExporter();
+
+ // Make sure to attach the logging instrumentation setting, so we can capture logs.
+ // Without this you'll need to configure logging yourself. Which is kind of a pain.
+ resourceBuilder.WithEnvironment("OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED", "true");
+ }
+
+ return resourceBuilder;
+ }
+
+ private static void AddProjectArguments(string scriptPath, string[] scriptArgs, CommandLineArgsCallbackContext context)
+ {
+ context.Args.Add("run");
+ context.Args.Add(scriptPath);
+
+ foreach (var arg in scriptArgs)
+ {
+ context.Args.Add(arg);
+ }
+ }
+
+ private static void AddOpenTelemetryArguments(CommandLineArgsCallbackContext context)
+ {
+ context.Args.Add("--traces_exporter");
+ context.Args.Add("otlp");
+
+ context.Args.Add("--logs_exporter");
+ context.Args.Add("console,otlp");
+
+ context.Args.Add("--metrics_exporter");
+ context.Args.Add("otlp");
+ }
+}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs
new file mode 100644
index 000000000..a8d8eaef8
--- /dev/null
+++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs
@@ -0,0 +1,12 @@
+using Aspire.Hosting.Python;
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// Represents a Streamlit application.
+///
+/// The name of the resource.
+/// The path to the executable used to run the python app.
+/// The working directory for streamlit.
+public class StreamlitAppResource(string name, string executablePath, string workingDirectory)
+ : PythonAppResource(name, executablePath, workingDirectory), IResourceWithServiceDiscovery;
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests/AppHostTests.cs
index d06685860..0db4eb21c 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests/AppHostTests.cs
+++ b/tests/CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests/AppHostTests.cs
@@ -8,6 +8,7 @@ public class AppHostTests(AspireIntegrationTestFixture();
+
+ var resource = appModel.Resources.OfType().SingleOrDefault();
+
+ Assert.NotNull(resource);
+
+ Assert.Equal("streamlit", resource.Command);
+ Assert.Equal(NormalizePathForCurrentPlatform("../../examples/python/streamlit-api"), resource.WorkingDirectory);
+ }
+
static string NormalizePathForCurrentPlatform(string path)
{
if (string.IsNullOrWhiteSpace(path) == true)
From 282844c9e02e0468115c97b88419995b6c02bde9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 6 Nov 2025 08:05:33 +0000
Subject: [PATCH 3/8] Add PORT to STREAMLIT_SERVER_PORT environment variable
mapping
Co-authored-by: tommasodotNET <12819039+tommasodotNET@users.noreply.github.com>
---
.../StreamlitAppHostingExtension.cs | 33 +++++++++++++------
1 file changed, 23 insertions(+), 10 deletions(-)
diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
index 1eb781491..32a621b5c 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
@@ -56,19 +56,28 @@ private static IResourceBuilder AddStreamlitApp(
var projectResource = new StreamlitAppResource(name, projectExecutable, projectDirectory);
- var resourceBuilder = builder.AddResource(projectResource).WithArgs(context =>
- {
- // If the project is to be automatically instrumented, add the instrumentation executable arguments first.
- if (!string.IsNullOrEmpty(instrumentationExecutable))
+ var resourceBuilder = builder.AddResource(projectResource)
+ .WithEnvironment(context =>
+ {
+ // Streamlit uses STREAMLIT_SERVER_PORT instead of PORT, so map PORT to STREAMLIT_SERVER_PORT
+ if (context.EnvironmentVariables.TryGetValue("PORT", out var portValue))
+ {
+ context.EnvironmentVariables["STREAMLIT_SERVER_PORT"] = portValue;
+ }
+ })
+ .WithArgs(context =>
{
- AddOpenTelemetryArguments(context);
+ // If the project is to be automatically instrumented, add the instrumentation executable arguments first.
+ if (!string.IsNullOrEmpty(instrumentationExecutable))
+ {
+ AddOpenTelemetryArguments(context);
- // Add the streamlit executable as the next argument so we can run the project.
- context.Args.Add("streamlit");
- }
+ // Add the streamlit executable as the next argument so we can run the project.
+ context.Args.Add("streamlit");
+ }
- AddProjectArguments(scriptPath, args, context);
- });
+ AddProjectArguments(scriptPath, args, context);
+ });
if (!string.IsNullOrEmpty(instrumentationExecutable))
{
@@ -87,6 +96,10 @@ private static void AddProjectArguments(string scriptPath, string[] scriptArgs,
context.Args.Add("run");
context.Args.Add(scriptPath);
+ // Add --server.headless to run without browser opening
+ context.Args.Add("--server.headless");
+ context.Args.Add("true");
+
foreach (var arg in scriptArgs)
{
context.Args.Add(arg);
From d19ecb32508dc5f75464df5c0e52852e72056127 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 6 Nov 2025 08:07:40 +0000
Subject: [PATCH 4/8] Add README for Streamlit example
---
examples/python/streamlit-api/README.md | 57 +++++++++++++++++++++++++
1 file changed, 57 insertions(+)
create mode 100644 examples/python/streamlit-api/README.md
diff --git a/examples/python/streamlit-api/README.md b/examples/python/streamlit-api/README.md
new file mode 100644
index 000000000..906937291
--- /dev/null
+++ b/examples/python/streamlit-api/README.md
@@ -0,0 +1,57 @@
+# Streamlit API Example
+
+This is a simple Streamlit application that demonstrates how to run Streamlit apps in .NET Aspire using the Python Extensions.
+
+## Prerequisites
+
+- Python 3.12 or later
+- .NET 8.0 or later
+- Streamlit package
+
+## Setup
+
+1. Create a virtual environment:
+ ```bash
+ python -m venv .venv
+ ```
+
+2. Activate the virtual environment:
+ - On Windows:
+ ```bash
+ .venv\Scripts\activate
+ ```
+ - On macOS/Linux:
+ ```bash
+ source .venv/bin/activate
+ ```
+
+3. Install dependencies:
+ ```bash
+ pip install streamlit
+ ```
+
+## Running the App
+
+The app can be run as part of the Aspire AppHost project or standalone.
+
+### Run with Aspire
+
+Navigate to the AppHost project and run:
+```bash
+dotnet run
+```
+
+### Run Standalone
+
+From this directory, run:
+```bash
+streamlit run app.py
+```
+
+## About the App
+
+This is a basic Streamlit app that demonstrates:
+- Simple text display
+- Environment variable reading (PORT)
+- Session state management
+- Interactive buttons
From 31c6448ba41f4213eea7b42b2a3434f6607664e0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 6 Nov 2025 08:33:04 +0000
Subject: [PATCH 5/8] Address review comments: remove .NET prefix and hardcode
streamlit executable
Co-authored-by: tommasodotNET <12819039+tommasodotNET@users.noreply.github.com>
---
examples/python/streamlit-api/README.md | 2 +-
examples/python/streamlit-api/app.py | 2 +-
.../StreamlitAppHostingExtension.cs | 45 +------------------
.../StreamlitAppResource.cs | 5 +--
4 files changed, 6 insertions(+), 48 deletions(-)
diff --git a/examples/python/streamlit-api/README.md b/examples/python/streamlit-api/README.md
index 906937291..04b5d7c87 100644
--- a/examples/python/streamlit-api/README.md
+++ b/examples/python/streamlit-api/README.md
@@ -1,6 +1,6 @@
# Streamlit API Example
-This is a simple Streamlit application that demonstrates how to run Streamlit apps in .NET Aspire using the Python Extensions.
+This is a simple Streamlit application that demonstrates how to run Streamlit apps in Aspire using the Python Extensions.
## Prerequisites
diff --git a/examples/python/streamlit-api/app.py b/examples/python/streamlit-api/app.py
index 07450f396..1c8b4f4ff 100644
--- a/examples/python/streamlit-api/app.py
+++ b/examples/python/streamlit-api/app.py
@@ -3,7 +3,7 @@
st.title("Hello, Aspire!")
-st.write("This is a simple Streamlit app running in .NET Aspire.")
+st.write("This is a simple Streamlit app running in Aspire.")
port = os.environ.get("PORT", "8501")
st.write(f"Running on port: {port}")
diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
index 32a621b5c..a3bdf2d97 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
@@ -1,5 +1,4 @@
using Aspire.Hosting.ApplicationModel;
-using Aspire.Hosting.Python;
using CommunityToolkit.Aspire.Utils;
namespace Aspire.Hosting;
@@ -46,17 +45,9 @@ private static IResourceBuilder AddStreamlitApp(
projectDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, wd));
- var virtualEnvironment = new VirtualEnvironment(Path.IsPathRooted(virtualEnvironmentPath)
- ? virtualEnvironmentPath
- : Path.Join(projectDirectory, virtualEnvironmentPath));
+ var projectResource = new StreamlitAppResource(name, projectDirectory);
- var instrumentationExecutable = virtualEnvironment.GetExecutable("opentelemetry-instrument");
- var streamlitExecutable = virtualEnvironment.GetExecutable("streamlit") ?? "streamlit";
- var projectExecutable = instrumentationExecutable ?? streamlitExecutable;
-
- var projectResource = new StreamlitAppResource(name, projectExecutable, projectDirectory);
-
- var resourceBuilder = builder.AddResource(projectResource)
+ return builder.AddResource(projectResource)
.WithEnvironment(context =>
{
// Streamlit uses STREAMLIT_SERVER_PORT instead of PORT, so map PORT to STREAMLIT_SERVER_PORT
@@ -67,28 +58,8 @@ private static IResourceBuilder AddStreamlitApp(
})
.WithArgs(context =>
{
- // If the project is to be automatically instrumented, add the instrumentation executable arguments first.
- if (!string.IsNullOrEmpty(instrumentationExecutable))
- {
- AddOpenTelemetryArguments(context);
-
- // Add the streamlit executable as the next argument so we can run the project.
- context.Args.Add("streamlit");
- }
-
AddProjectArguments(scriptPath, args, context);
});
-
- if (!string.IsNullOrEmpty(instrumentationExecutable))
- {
- resourceBuilder.WithOtlpExporter();
-
- // Make sure to attach the logging instrumentation setting, so we can capture logs.
- // Without this you'll need to configure logging yourself. Which is kind of a pain.
- resourceBuilder.WithEnvironment("OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED", "true");
- }
-
- return resourceBuilder;
}
private static void AddProjectArguments(string scriptPath, string[] scriptArgs, CommandLineArgsCallbackContext context)
@@ -105,16 +76,4 @@ private static void AddProjectArguments(string scriptPath, string[] scriptArgs,
context.Args.Add(arg);
}
}
-
- private static void AddOpenTelemetryArguments(CommandLineArgsCallbackContext context)
- {
- context.Args.Add("--traces_exporter");
- context.Args.Add("otlp");
-
- context.Args.Add("--logs_exporter");
- context.Args.Add("console,otlp");
-
- context.Args.Add("--metrics_exporter");
- context.Args.Add("otlp");
- }
}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs
index a8d8eaef8..08f69dd5a 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs
@@ -6,7 +6,6 @@ namespace Aspire.Hosting.ApplicationModel;
/// Represents a Streamlit application.
///
/// The name of the resource.
-/// The path to the executable used to run the python app.
/// The working directory for streamlit.
-public class StreamlitAppResource(string name, string executablePath, string workingDirectory)
- : PythonAppResource(name, executablePath, workingDirectory), IResourceWithServiceDiscovery;
+public class StreamlitAppResource(string name, string workingDirectory)
+ : PythonAppResource(name, "streamlit", workingDirectory), IResourceWithServiceDiscovery;
From 0b68f842aaddafa2f5ee247065870694b3ae490b Mon Sep 17 00:00:00 2001
From: Tommaso Stocchi
Date: Thu, 6 Nov 2025 11:09:49 +0100
Subject: [PATCH 6/8] Update
src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md
index c1ff5c440..98b7edada 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md
+++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md
@@ -39,7 +39,7 @@ var uvicorn = builder.AddUvApp("uvapp", "../uv-api", "uv-api")
### Streamlit example usage
-Then, in the _Program.cs_ file of `AddStreamlitApp`, define a Streamlit resource, then call `Add`:
+Then, in the _Program.cs_ file of your AppHost project, define a Streamlit resource, then call `Add`:
```csharp
var streamlit = builder.AddStreamlitApp("streamlitapp", "../streamlit-api", "app.py")
From 4b680f73b1ab72e4a4b85da6b582af7ccc120ccb Mon Sep 17 00:00:00 2001
From: Tommaso Stocchi
Date: Thu, 6 Nov 2025 11:11:34 +0100
Subject: [PATCH 7/8] Update
src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../StreamlitAppHostingExtension.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
index a3bdf2d97..d526fb48d 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
@@ -43,7 +43,9 @@ private static IResourceBuilder AddStreamlitApp(
string wd = projectDirectory ?? Path.Combine("..", name);
- projectDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(builder.AppHostDirectory, wd));
+ var normalizedAppHostDirectory = PathNormalizer.NormalizePathForCurrentPlatform(builder.AppHostDirectory);
+ var normalizedWd = PathNormalizer.NormalizePathForCurrentPlatform(wd);
+ projectDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(normalizedAppHostDirectory, normalizedWd));
var projectResource = new StreamlitAppResource(name, projectDirectory);
From 0e4a21165a4127f45abc54adc455d21394146fcd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 11 Nov 2025 01:05:19 +0000
Subject: [PATCH 8/8] Update StreamlitApp to use Aspire.Hosting.Python
integration and mark as experimental
Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
---
.../Program.cs | 1 +
.../StreamlitAppHostingExtension.cs | 119 +++++++++++-------
.../StreamlitAppResource.cs | 5 +-
.../ResourceCreationTests.cs | 4 +-
4 files changed, 78 insertions(+), 51 deletions(-)
diff --git a/examples/python/CommunityToolkit.Aspire.Hosting.Python.Extensions.AppHost/Program.cs b/examples/python/CommunityToolkit.Aspire.Hosting.Python.Extensions.AppHost/Program.cs
index be72739f9..66e43ead8 100644
--- a/examples/python/CommunityToolkit.Aspire.Hosting.Python.Extensions.AppHost/Program.cs
+++ b/examples/python/CommunityToolkit.Aspire.Hosting.Python.Extensions.AppHost/Program.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS0612 // Type or member is obsolete
#pragma warning disable CS0618 // Type or member is obsolete
+#pragma warning disable CTASPIRE001 // Experimental API
var builder = DistributedApplication.CreateBuilder(args);
builder.AddUvicornApp("uvicornapp", "../uvicornapp-api", "main:app");
diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
index d526fb48d..d2ed5e76d 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs
@@ -1,5 +1,7 @@
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.ApplicationModel;
-using CommunityToolkit.Aspire.Utils;
+using Aspire.Hosting.Python;
namespace Aspire.Hosting;
@@ -9,73 +11,94 @@ namespace Aspire.Hosting;
public static class StreamlitAppHostingExtension
{
///
- /// Adds a Streamlit application to the distributed application builder.
+ /// Adds a Streamlit application to the application model.
///
- /// The distributed application builder.
+ /// The to add the resource to.
/// The name of the Streamlit application.
- /// The directory of the project containing the Streamlit application.
- /// The path to the Python script to be run by Streamlit.
- /// Optional arguments to pass to the Streamlit command.
+ /// The path to the directory containing the Streamlit application.
+ /// The path to the Python script to be run by Streamlit (relative to appDirectory).
/// An for the Streamlit application resource.
- /// Thrown if is null.
- /// Thrown if or is null or empty.
+ ///
+ ///
+ /// This method uses the Aspire.Hosting.Python integration to run Streamlit applications.
+ /// By default, it uses the .venv virtual environment in the app directory.
+ /// Use standard Python extension methods like WithVirtualEnvironment, WithPip, or WithUv to customize the environment.
+ ///
+ ///
+ /// **⚠️ EXPERIMENTAL:** This integration is experimental and subject to change. The underlying implementation
+ /// will be updated to use public APIs when they become available in Aspire.Hosting.Python (expected in Aspire 13.1).
+ ///
+ ///
+ ///
+ /// Add a Streamlit application to the application model:
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ ///
+ /// builder.AddStreamlitApp("dashboard", "../streamlit-app", "app.py")
+ /// .WithHttpEndpoint(env: "PORT");
+ ///
+ /// builder.Build().Run();
+ ///
+ ///
+ [Experimental("CTASPIRE001", UrlFormat = "https://github.com/CommunityToolkit/Aspire/issues/{0}")]
public static IResourceBuilder AddStreamlitApp(
this IDistributedApplicationBuilder builder,
[ResourceName] string name,
- string projectDirectory,
- string scriptPath,
- params string[] args)
- {
- return builder.AddStreamlitApp(name, projectDirectory, scriptPath, ".venv", args);
- }
-
- private static IResourceBuilder AddStreamlitApp(
- this IDistributedApplicationBuilder builder,
- string name,
- string projectDirectory,
- string scriptPath,
- string virtualEnvironmentPath,
- params string[] args)
+ string appDirectory,
+ string scriptPath)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ ArgumentNullException.ThrowIfNull(appDirectory);
ArgumentException.ThrowIfNullOrWhiteSpace(scriptPath);
- string wd = projectDirectory ?? Path.Combine("..", name);
+ // Use AddPythonExecutable to run streamlit from the virtual environment
+ var pythonBuilder = builder.AddPythonExecutable(name, appDirectory, "streamlit")
+ .WithDebugging()
+ .WithHttpEndpoint(env: "PORT")
+ .WithArgs(context =>
+ {
+ context.Args.Add("run");
+ context.Args.Add(scriptPath);
- var normalizedAppHostDirectory = PathNormalizer.NormalizePathForCurrentPlatform(builder.AppHostDirectory);
- var normalizedWd = PathNormalizer.NormalizePathForCurrentPlatform(wd);
- projectDirectory = PathNormalizer.NormalizePathForCurrentPlatform(Path.Combine(normalizedAppHostDirectory, normalizedWd));
+ // Add --server.headless to run without browser opening
+ context.Args.Add("--server.headless");
+ context.Args.Add("true");
- var projectResource = new StreamlitAppResource(name, projectDirectory);
+ // Configure server port from endpoint
+ var endpoint = ((IResourceWithEndpoints)context.Resource).GetEndpoint("http");
+ context.Args.Add("--server.port");
+ context.Args.Add(endpoint.Property(EndpointProperty.TargetPort));
- return builder.AddResource(projectResource)
- .WithEnvironment(context =>
- {
- // Streamlit uses STREAMLIT_SERVER_PORT instead of PORT, so map PORT to STREAMLIT_SERVER_PORT
- if (context.EnvironmentVariables.TryGetValue("PORT", out var portValue))
+ // Configure server address
+ context.Args.Add("--server.address");
+ if (builder.ExecutionContext.IsPublishMode)
{
- context.EnvironmentVariables["STREAMLIT_SERVER_PORT"] = portValue;
+ context.Args.Add("0.0.0.0");
+ }
+ else
+ {
+ context.Args.Add(endpoint.EndpointAnnotation.TargetHost);
}
- })
- .WithArgs(context =>
- {
- AddProjectArguments(scriptPath, args, context);
});
- }
- private static void AddProjectArguments(string scriptPath, string[] scriptArgs, CommandLineArgsCallbackContext context)
- {
- context.Args.Add("run");
- context.Args.Add(scriptPath);
-
- // Add --server.headless to run without browser opening
- context.Args.Add("--server.headless");
- context.Args.Add("true");
+ // Create a StreamlitAppResource wrapping the PythonAppResource
+ // This allows for Streamlit-specific extension methods in the future
+ var streamlitResource = new StreamlitAppResource(
+ pythonBuilder.Resource.Name,
+ pythonBuilder.Resource.Command,
+ pythonBuilder.Resource.WorkingDirectory);
- foreach (var arg in scriptArgs)
+ // Copy annotations from the Python resource
+ foreach (var annotation in pythonBuilder.Resource.Annotations)
{
- context.Args.Add(arg);
+ streamlitResource.Annotations.Add(annotation);
}
+
+ // Replace the resource in the builder
+ builder.Resources.Remove(pythonBuilder.Resource);
+ var streamlitBuilder = builder.AddResource(streamlitResource);
+
+ return streamlitBuilder;
}
}
diff --git a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs
index 08f69dd5a..02aa1a2c3 100644
--- a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs
+++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppResource.cs
@@ -6,6 +6,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// Represents a Streamlit application.
///
/// The name of the resource.
+/// The path to the executable used to run the Streamlit app.
/// The working directory for streamlit.
-public class StreamlitAppResource(string name, string workingDirectory)
- : PythonAppResource(name, "streamlit", workingDirectory), IResourceWithServiceDiscovery;
+public class StreamlitAppResource(string name, string executablePath, string workingDirectory)
+ : PythonAppResource(name, executablePath, workingDirectory), IResourceWithServiceDiscovery;
diff --git a/tests/CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests/ResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests/ResourceCreationTests.cs
index 92e76445d..72caf536e 100644
--- a/tests/CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests/ResourceCreationTests.cs
+++ b/tests/CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests/ResourceCreationTests.cs
@@ -4,6 +4,7 @@ namespace CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests;
#pragma warning disable CS0612 // Type or member is obsolete
#pragma warning disable CS0618 // Type or member is obsolete
+#pragma warning disable CTASPIRE001 // Experimental API
public class ResourceCreationTests
{
[Fact(Skip = "Being removed with https://github.com/CommunityToolkit/Aspire/issues/917")]
@@ -59,7 +60,8 @@ public void DefaultStreamlitApp()
Assert.NotNull(resource);
- Assert.Equal("streamlit", resource.Command);
+ // Command will be the full path to streamlit executable in venv, so just check it contains "streamlit"
+ Assert.Contains("streamlit", resource.Command);
Assert.Equal(NormalizePathForCurrentPlatform("../../examples/python/streamlit-api"), resource.WorkingDirectory);
}