Skip to content

Commit

Permalink
221 python uv (#387)
Browse files Browse the repository at this point in the history
* adds uv example

* unshipped api definition for uv

* working uv integration

* adds doc

* adds test

* removes comment for pythonexecutable

* installs uv on devcontainer

* added uv setup in cicd

* fixes project.toml

---------

Co-authored-by: Alireza Baloochi <[email protected]>
  • Loading branch information
tommasodotNET and Alirexaa authored Jan 15, 2025
1 parent 254101c commit 66046c2
Show file tree
Hide file tree
Showing 16 changed files with 206 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ curl -fsSL https://bun.sh/install | bash
echo Installing uvicorn
pip install uvicorn

echo Installing uv
pip install uv

echo Done!
2 changes: 2 additions & 0 deletions .github/workflows/dotnet-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install uvicorn
- uses: astral-sh/setup-uv@v5
name: Install uv
- uses: actions/setup-go@v5
name: Set up Go
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/dotnet-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install uvicorn
- uses: astral-sh/setup-uv@v5
name: Install uv
- uses: actions/setup-go@v5
name: Set up Go
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/dotnet-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install uvicorn
- uses: astral-sh/setup-uv@v5
name: Install uv
- uses: actions/setup-go@v5
name: Set up Go
with:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#pragma warning disable CS0612
var builder = DistributedApplication.CreateBuilder(args);

var uvicorn = builder.AddUvicornApp("uvicornapp", "../uvicornapp-api", "main:app")
.WithHttpEndpoint(env: "UVICORN_PORT");

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

builder.Build().Run();
3 changes: 3 additions & 0 deletions examples/python/uv-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.venv
uv.lock
**/__pycache__/
1 change: 1 addition & 0 deletions examples/python/uv-api/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
19 changes: 19 additions & 0 deletions examples/python/uv-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[project]
name = "uv-api"
version = "0.1.0"
description = "Test project for uv-api"
authors = [
{ name = "Tommaso Stocchi", email = "[email protected]" }
]
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.6",
"uvicorn>=0.32.1",
]

[project.scripts]
uv-api = "uv_api:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
13 changes: 13 additions & 0 deletions examples/python/uv-api/src/uv_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from fastapi import FastAPI
import uvicorn
import os

app = FastAPI()

@app.get("/")
def read_root():
return {"message": "Hello, World!"}

def main() -> None:
port = int(os.environ.get("PORT", 8000))
uvicorn.run(app, host="127.0.0.1", port=port)
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
#nullable enable

Aspire.Hosting.ApplicationModel.UvAppResource
Aspire.Hosting.ApplicationModel.UvAppResource.UvAppResource(string! name, string! workingDirectory) -> void
Aspire.Hosting.UvAppHostingExtension
static Aspire.Hosting.UvAppHostingExtension.AddUvApp(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! projectDirectory, string! scriptPath, params string![]! scriptArgs) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.UvAppResource!>!
15 changes: 13 additions & 2 deletions src/CommunityToolkit.Aspire.Hosting.Python.Extensions/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# CommunityToolkit.Aspire.Hosting.Python.Extensions library

Provides extensions methods and resource definitions for the .NET Aspire AppHost to support running Uvicorn applications.
Provides extensions methods and resource definitions for the .NET Aspire AppHost to extend the support for Python applications. Current support includes:
- Uvicorn
- Uv

## Getting Started

Expand All @@ -16,7 +18,7 @@ dotnet add package CommunityToolkit.Aspire.Hosting.Python.Extensions

Please refer to the [Python virtual environment](https://learn.microsoft.com/dotnet/aspire/get-started/build-aspire-apps-with-python?tabs=powershell#initialize-the-python-virtual-environment) section for more information.

### Example usage
### Uvicorn example usage

Then, in the _Program.cs_ file of `AddUvicornApp`, define a Uvicorn resource, then call `Add`:

Expand All @@ -25,6 +27,15 @@ var uvicorn = builder.AddUvicornApp("uvicornapp", "../uvicornapp-api", "main:app
.WithHttpEndpoint(env: "UVICORN_PORT");
```

### Uv example usage

Then, in the _Program.cs_ file of `AddUvApp`, define a Uvicorn resource, then call `Add`:

```csharp
var uvicorn = builder.AddUvApp("uvapp", "../uv-api", "uv-api")
.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,99 @@
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Utils;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for adding Uv applications to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class UvAppHostingExtension
{
/// <summary>
/// Adds a Uv application to the distributed application builder.
/// </summary>
/// <param name="builder">The distributed application builder.</param>
/// <param name="name">The name of the Uv application.</param>
/// <param name="projectDirectory">The directory of the project containing the Uv application.</param>
/// <param name="scriptPath">The name of the Uv app.</param>
/// <param name="scriptArgs">Optional arguments to pass to the script.</param>
/// <returns>An <see cref="IResourceBuilder{UvAppResource}"/> for the Uv application resource.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="builder"/> is null.</exception>
public static IResourceBuilder<UvAppResource> AddUvApp(
this IDistributedApplicationBuilder builder,
string name,
string projectDirectory,
string scriptPath,
params string[] scriptArgs)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.AddUvApp(name, scriptPath, projectDirectory, ".venv", scriptArgs);
}

private static IResourceBuilder<UvAppResource> AddUvApp(this IDistributedApplicationBuilder builder,
string name,
string scriptPath,
string projectDirectory,
string virtualEnvironmentPath,
params string[] args)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(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 pythonExecutable = virtualEnvironment.GetRequiredExecutable("python");
// var projectExecutable = instrumentationExecutable ?? pythonExecutable;

string[] allArgs = args is { Length: > 0 }
? ["run", scriptPath, .. args]
: ["run", scriptPath];

var projectResource = new UvAppResource(name, projectDirectory);

var resourceBuilder = builder.AddResource(projectResource)
.WithArgs(allArgs)
.WithArgs(context =>
{
// If the project is to be automatically instrumented, add the instrumentation executable arguments first.
if (!string.IsNullOrEmpty(instrumentationExecutable))
{
AddOpenTelemetryArguments(context);

// // Add the python executable as the next argument so we can run the project.
// context.Args.Add(pythonExecutable!);
}
});

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

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Represents a Uv application.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="workingDirectory">The working directory for uv.</param>
public class UvAppResource(string name, string workingDirectory)
: PythonAppResource(name, "uv", workingDirectory), IResourceWithServiceDiscovery
{
internal const string HttpEndpointName = "http";
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Python;
using CommunityToolkit.Aspire.Utils;

namespace Aspire.Hosting;
Expand Down Expand Up @@ -64,9 +65,6 @@ private static IResourceBuilder<UvicornAppResource> AddUvicornApp(this IDistribu
if (!string.IsNullOrEmpty(instrumentationExecutable))
{
AddOpenTelemetryArguments(context);

// // Add the python executable as the next argument so we can run the project.
// context.Args.Add(pythonExecutable!);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ namespace CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests;
#pragma warning disable CTASPIRE001
public class AppHostTests(AspireIntegrationTestFixture<Projects.CommunityToolkit_Aspire_Hosting_Python_Extensions_AppHost> fixture) : IClassFixture<AspireIntegrationTestFixture<Projects.CommunityToolkit_Aspire_Hosting_Python_Extensions_AppHost>>
{
[Fact]
public async Task ResourceStartsAndRespondsOk()
[Theory]
[InlineData("uvicornapp")]
[InlineData("uvapp")]
public async Task ResourceStartsAndRespondsOk(string appName)
{
var appName = "uvicornapp";
var httpClient = fixture.CreateHttpClient(appName);

await fixture.App.WaitForTextAsync("Uvicorn running on", appName).WaitAsync(TimeSpan.FromMinutes(5));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

namespace CommunityToolkit.Aspire.Hosting.Python.Extensions.Tests;

#pragma warning disable CS0612
public class ResourceCreationTests
{
[Fact]
public void DefaultUvicornApp()
{
var builder = DistributedApplication.CreateBuilder();

builder.AddUvicornApp("uvicornapp", "../../examples/uvicorn/uvicornapp-api", "main:app");
builder.AddUvicornApp("uvicornapp", "../../examples/python/uvicornapp-api", "main:app");

using var app = builder.Build();

Expand All @@ -20,7 +21,26 @@ public void DefaultUvicornApp()
Assert.NotNull(resource);

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

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

builder.AddUvApp("uvapp", "../../examples/python/uv-api", "uv-api");

using var app = builder.Build();

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

var resource = appModel.Resources.OfType<UvAppResource>().SingleOrDefault();

Assert.NotNull(resource);

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

static string NormalizePathForCurrentPlatform(string path)
Expand Down

0 comments on commit 66046c2

Please sign in to comment.