Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#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");

builder.AddUvApp("uvapp", "../uv-api", "uv-api")
.WithHttpEndpoint(env: "PORT");

builder.AddStreamlitApp("streamlitapp", "../streamlit-api", "app.py")
.WithHttpEndpoint(env: "PORT");

builder.Build().Run();
3 changes: 3 additions & 0 deletions examples/python/streamlit-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.venv/
__pycache__/
*.pyc
1 change: 1 addition & 0 deletions examples/python/streamlit-api/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
57 changes: 57 additions & 0 deletions examples/python/streamlit-api/README.md
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions examples/python/streamlit-api/app.py
Original file line number Diff line number Diff line change
@@ -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")
12 changes: 12 additions & 0 deletions examples/python/streamlit-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
10 changes: 10 additions & 0 deletions src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Python;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for adding Streamlit applications to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class StreamlitAppHostingExtension
{
/// <summary>
/// Adds a Streamlit application to the application model.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/> to add the resource to.</param>
/// <param name="name">The name of the Streamlit application.</param>
/// <param name="appDirectory">The path to the directory containing the Streamlit application.</param>
/// <param name="scriptPath">The path to the Python script to be run by Streamlit (relative to appDirectory).</param>
/// <returns>An <see cref="IResourceBuilder{StreamlitAppResource}"/> for the Streamlit application resource.</returns>
/// <remarks>
/// <para>
/// This method uses the Aspire.Hosting.Python integration to run Streamlit applications.
/// By default, it uses the <c>.venv</c> virtual environment in the app directory.
/// Use standard Python extension methods like <c>WithVirtualEnvironment</c>, <c>WithPip</c>, or <c>WithUv</c> to customize the environment.
/// </para>
/// <para>
/// **⚠️ 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).
/// </para>
/// </remarks>
/// <example>
/// Add a Streamlit application to the application model:
/// <code lang="csharp">
/// var builder = DistributedApplication.CreateBuilder(args);
///
/// builder.AddStreamlitApp("dashboard", "../streamlit-app", "app.py")
/// .WithHttpEndpoint(env: "PORT");
///
/// builder.Build().Run();
/// </code>
/// </example>
[Experimental("CTASPIRE001", UrlFormat = "https://github.com/CommunityToolkit/Aspire/issues/{0}")]
public static IResourceBuilder<StreamlitAppResource> AddStreamlitApp(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does debugging and deploying work?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't invest in that until 13.1 when we can drop our implementation of the internals of the Python integration.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make sure it works. The bar should be higher in 13 for new language / framework integrations. Even if it doesn't work I don't want us to merge things without understanding what works and what doesn't work. I'd expect the future to look like a set of capabililities for these language integrations (otel support, endpoints, https, dockerfile, debugging). Doing enough work to know what we will document is a minimum bar here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I appreciate the desire, we have to work with the realities of the OSS project - we don't have the resources to do everything that Aspire does.

The goal is to do our best to be producing high quality integrations to the level of Aspire, but it's not always going to be the case, especially if we want to ship. Feel free to raise an issue to get debugger support enabled, but I don't see it as a blocker for us shipping.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok let me ask then. Does it work when you run locally and deploy? If it doesn’t? Do we know why? Has anyone built an app with this PR?

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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Aspire.Hosting.Python;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Represents a Streamlit application.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="executablePath">The path to the executable used to run the Streamlit app.</param>
/// <param name="workingDirectory">The working directory for streamlit.</param>
public class StreamlitAppResource(string name, string executablePath, string workingDirectory)
: PythonAppResource(name, executablePath, workingDirectory), IResourceWithServiceDiscovery;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class AppHostTests(AspireIntegrationTestFixture<Projects.CommunityToolkit
[Theory(Skip = "To be reviewed with https://github.com/CommunityToolkit/Aspire/issues/917")]
[InlineData("uvicornapp")]
[InlineData("uvapp")]
[InlineData("streamlitapp")]
public async Task ResourceStartsAndRespondsOk(string appName)
{
var httpClient = fixture.CreateHttpClient(appName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -44,6 +45,26 @@ public void DefaultUvApp()
Assert.Equal(NormalizePathForCurrentPlatform("../../examples/python/uv-api"), resource.WorkingDirectory);
}

[Fact]
public void DefaultStreamlitApp()
{
var builder = DistributedApplication.CreateBuilder();

builder.AddStreamlitApp("streamlitapp", "../../examples/python/streamlit-api", "app.py");

using var app = builder.Build();

var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var resource = appModel.Resources.OfType<StreamlitAppResource>().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)
Expand Down
Loading