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 fd0fe3ad..66e43ead 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"); @@ -7,4 +8,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 00000000..77ac7549 --- /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 00000000..e4fba218 --- /dev/null +++ b/examples/python/streamlit-api/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/examples/python/streamlit-api/README.md b/examples/python/streamlit-api/README.md new file mode 100644 index 00000000..04b5d7c8 --- /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 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 diff --git a/examples/python/streamlit-api/app.py b/examples/python/streamlit-api/app.py new file mode 100644 index 00000000..1c8b4f4f --- /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 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 00000000..390870e1 --- /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 e9ff639f..aa2c241e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md @@ -13,6 +13,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 @@ -46,6 +47,15 @@ var uvicorn = builder.AddUvApp("uvapp", "../uv-api", "uv-api") .WithHttpEndpoint(env: "PORT"); ``` +### Streamlit example usage + +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") + .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 00000000..d2ed5e76 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.Python.Extensions/StreamlitAppHostingExtension.cs @@ -0,0 +1,104 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Python; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Streamlit applications to an . +/// +public static class StreamlitAppHostingExtension +{ + /// + /// Adds a Streamlit application to the application model. + /// + /// The to add the resource to. + /// The name of the Streamlit application. + /// 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. + /// + /// + /// 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 appDirectory, + string scriptPath) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrWhiteSpace(name); + ArgumentNullException.ThrowIfNull(appDirectory); + ArgumentException.ThrowIfNullOrWhiteSpace(scriptPath); + + // 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); + + // Add --server.headless to run without browser opening + context.Args.Add("--server.headless"); + context.Args.Add("true"); + + // Configure server port from endpoint + var endpoint = ((IResourceWithEndpoints)context.Resource).GetEndpoint("http"); + context.Args.Add("--server.port"); + context.Args.Add(endpoint.Property(EndpointProperty.TargetPort)); + + // Configure server address + context.Args.Add("--server.address"); + if (builder.ExecutionContext.IsPublishMode) + { + context.Args.Add("0.0.0.0"); + } + else + { + context.Args.Add(endpoint.EndpointAnnotation.TargetHost); + } + }); + + // 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); + + // Copy annotations from the Python resource + foreach (var annotation in pythonBuilder.Resource.Annotations) + { + 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 new file mode 100644 index 00000000..02aa1a2c --- /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 Streamlit 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 de0d94f1..b68e6566 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests/AppHostTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests/AppHostTests.cs @@ -10,6 +10,7 @@ public class AppHostTests(AspireIntegrationTestFixture(); + + var resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + + // 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); + } + static string NormalizePathForCurrentPlatform(string path) { if (string.IsNullOrWhiteSpace(path) == true)