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); }