Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Expand Up @@ -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();
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 @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Utils;

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 distributed application builder.
/// </summary>
/// <param name="builder">The distributed application builder.</param>
/// <param name="name">The name of the Streamlit application.</param>
/// <param name="projectDirectory">The directory of the project containing the Streamlit application.</param>
/// <param name="scriptPath">The path to the Python script to be run by Streamlit.</param>
/// <param name="args">Optional arguments to pass to the Streamlit command.</param>
/// <returns>An <see cref="IResourceBuilder{StreamlitAppResource}"/> for the Streamlit application resource.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="builder"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="name"/> or <paramref name="scriptPath"/> is null or empty.</exception>
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 projectDirectory,
string scriptPath,
params string[] args)
{
return builder.AddStreamlitApp(name, projectDirectory, scriptPath, ".venv", args);
}

private static IResourceBuilder<StreamlitAppResource> 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 projectResource = new StreamlitAppResource(name, projectDirectory);

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))
{
context.EnvironmentVariables["STREAMLIT_SERVER_PORT"] = portValue;
}
})
.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");

foreach (var arg in scriptArgs)
{
context.Args.Add(arg);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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="workingDirectory">The working directory for streamlit.</param>
public class StreamlitAppResource(string name, string workingDirectory)
: PythonAppResource(name, "streamlit", workingDirectory), IResourceWithServiceDiscovery;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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 @@ -43,6 +43,25 @@ 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);

Assert.Equal("streamlit", resource.Command);
Assert.Equal(NormalizePathForCurrentPlatform("../../examples/python/streamlit-api"), resource.WorkingDirectory);
}

static string NormalizePathForCurrentPlatform(string path)
{
if (string.IsNullOrWhiteSpace(path) == true)
Expand Down