-
Notifications
You must be signed in to change notification settings - Fork 940
Add WithPip following Node.js patterns and rename AddPythonScript to AddPythonApp #12667
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
7935c5d
d667fae
d56a491
ccbf1df
aac0897
1577270
b3c803c
b4b83d0
51f1894
ba1f423
0b0ba61
e51f072
8fc5073
9c8026e
b9f1ec1
3a56b22
cf15d22
3b02a9c
1a96d8b
2dc56ce
14c5d98
d44a5dc
f475d27
58fc26b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -496,6 +496,17 @@ private static IResourceBuilder<T> AddPythonAppCore<T>( | |
| } | ||
| }); | ||
|
|
||
| // Automatically add pip as the package manager if a requirements.txt file exists | ||
| // Only do this in run mode since the installer resource only runs in run mode | ||
| if (builder.ExecutionContext.IsRunMode) | ||
| { | ||
| var appDirectoryFullPath = Path.GetFullPath(appDirectory, builder.AppHostDirectory); | ||
| if (File.Exists(Path.Combine(appDirectoryFullPath, "requirements.txt"))) | ||
| { | ||
| resourceBuilder.WithPip(); | ||
| } | ||
| } | ||
|
|
||
| return resourceBuilder; | ||
| } | ||
|
|
||
|
|
@@ -1063,81 +1074,203 @@ public static IResourceBuilder<T> WithEntrypoint<T>( | |
| return builder; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Configures the Python resource to use pip as the package manager and optionally installs packages before the application starts. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of the Python application resource, must derive from <see cref="PythonAppResource"/>.</typeparam> | ||
| /// <param name="builder">The resource builder.</param> | ||
| /// <param name="install">When true (default), automatically installs packages before the application starts. When false, only sets the package manager annotation without creating an installer resource.</param> | ||
| /// <param name="installArgs">The command-line arguments passed to "pip install -r requirements.txt".</param> | ||
| /// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for method chaining.</returns> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// This method creates a child resource that runs <c>pip install -r requirements.txt</c> in the working directory of the Python application. | ||
| /// The Python application will wait for this resource to complete successfully before starting. | ||
| /// </para> | ||
| /// <para> | ||
| /// By default, if a requirements.txt file exists in the application directory, pip will install the dependencies. | ||
| /// Calling this method will replace any previously configured package manager (such as uv). | ||
| /// </para> | ||
| /// </remarks> | ||
| /// <example> | ||
| /// Add a Python app with automatic pip package installation: | ||
| /// <code lang="csharp"> | ||
| /// var builder = DistributedApplication.CreateBuilder(args); | ||
| /// | ||
| /// var python = builder.AddPythonScript("api", "../python-api", "main.py") | ||
| /// .WithPip() // Automatically runs 'pip install -r requirements.txt' before starting the app | ||
| /// .WithHttpEndpoint(port: 5000); | ||
| /// | ||
| /// builder.Build().Run(); | ||
| /// </code> | ||
| /// </example> | ||
| /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> is null.</exception> | ||
| public static IResourceBuilder<T> WithPip<T>(this IResourceBuilder<T> builder, bool install = true, string[]? installArgs = null) | ||
| where T : PythonAppResource | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
|
|
||
| // Get or create the virtual environment from the annotation | ||
| if (!builder.Resource.TryGetLastAnnotation<PythonEnvironmentAnnotation>(out var pythonEnv) || | ||
| pythonEnv.VirtualEnvironment is null) | ||
| { | ||
| throw new InvalidOperationException("Cannot add pip installer: Python environment annotation with virtual environment not found."); | ||
| } | ||
|
|
||
| var virtualEnvironment = pythonEnv.VirtualEnvironment; | ||
|
|
||
| builder | ||
| .WithAnnotation(new PythonPackageManagerAnnotation(virtualEnvironment.GetExecutable("pip")), ResourceAnnotationMutationBehavior.Replace) | ||
| .WithAnnotation(new PythonInstallCommandAnnotation(["install", "-r", "requirements.txt", .. installArgs ?? []]), ResourceAnnotationMutationBehavior.Replace); | ||
|
|
||
| AddInstaller(builder, install); | ||
| return builder; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Adds a UV environment setup task to ensure the virtual environment exists before running the Python application. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of the Python application resource, must derive from <see cref="PythonAppResource"/>.</typeparam> | ||
| /// <param name="builder">The resource builder.</param> | ||
| /// <param name="install">When true (default), automatically runs uv sync before the application starts. When false, only sets the package manager annotation without creating an installer resource.</param> | ||
| /// <param name="args">Optional custom arguments to pass to the uv sync command. If not provided, defaults to ["sync", "--python"].</param> | ||
| /// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for method chaining.</returns> | ||
| /// <remarks> | ||
| /// <para> | ||
| /// This method creates a child resource that runs <c>uv sync</c> in the working directory of the Python application. | ||
| /// This method creates a child resource that runs <c>uv sync --python</c> in the working directory of the Python application. | ||
| /// The Python application will wait for this resource to complete successfully before starting. | ||
| /// </para> | ||
| /// <para> | ||
| /// UV (https://github.com/astral-sh/uv) is a modern Python package manager written in Rust that can manage virtual environments | ||
| /// and dependencies with significantly faster performance than traditional tools. The <c>uv sync</c> command ensures that the virtual | ||
| /// environment exists and all dependencies specified in pyproject.toml are installed and synchronized. | ||
| /// and dependencies with significantly faster performance than traditional tools. The <c>uv sync --python</c> command ensures that the virtual | ||
| /// environment exists, Python is installed if needed, and all dependencies specified in pyproject.toml are installed and synchronized. | ||
| /// </para> | ||
| /// <para> | ||
| /// This method is idempotent - calling it multiple times on the same resource will not create duplicate UV environment resources. | ||
| /// If a UV environment resource already exists for the Python application, it will be reused. | ||
| /// Calling this method will replace any previously configured package manager (such as pip). | ||
| /// </para> | ||
| /// </remarks> | ||
| /// <example> | ||
| /// Add a Python app with automatic UV environment setup: | ||
| /// <code lang="csharp"> | ||
| /// var builder = DistributedApplication.CreateBuilder(args); | ||
| /// | ||
| /// var python = builder.AddPythonApp("api", "../python-api", "main.py") | ||
| /// .WithUvEnvironment() // Automatically runs 'uv sync' before starting the app | ||
| /// var python = builder.AddPythonScript("api", "../python-api", "main.py") | ||
| /// .WithUv() // Automatically runs 'uv sync --python' before starting the app | ||
| /// .WithHttpEndpoint(port: 5000); | ||
| /// | ||
| /// builder.Build().Run(); | ||
| /// </code> | ||
| /// </example> | ||
| /// <example> | ||
| /// Add a Python app with custom UV arguments: | ||
| /// <code lang="csharp"> | ||
| /// var builder = DistributedApplication.CreateBuilder(args); | ||
| /// | ||
| /// var python = builder.AddPythonScript("api", "../python-api", "main.py") | ||
| /// .WithUv(args: ["sync", "--python", "3.12", "--no-dev"]) // Custom uv sync args | ||
| /// .WithHttpEndpoint(port: 5000); | ||
| /// | ||
| /// builder.Build().Run(); | ||
| /// </code> | ||
| /// </example> | ||
| /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> is null.</exception> | ||
| /// <exception cref="DistributedApplicationException"> | ||
| /// Thrown when a resource with the UV environment name already exists but is not a <see cref="PythonUvEnvironmentResource"/>. | ||
| /// </exception> | ||
| public static IResourceBuilder<T> WithUvEnvironment<T>(this IResourceBuilder<T> builder) | ||
| public static IResourceBuilder<T> WithUv<T>(this IResourceBuilder<T> builder, bool install = true, string[]? args = null) | ||
| where T : PythonAppResource | ||
| { | ||
| ArgumentNullException.ThrowIfNull(builder); | ||
|
|
||
| var uvEnvironmentName = $"{builder.Resource.Name}-uv-environment"; | ||
| // Default args include --python flag to bootstrap Python if needed | ||
| args ??= ["sync", "--python"]; | ||
|
|
||
| builder | ||
| .WithAnnotation(new PythonPackageManagerAnnotation("uv"), ResourceAnnotationMutationBehavior.Replace) | ||
| .WithAnnotation(new PythonInstallCommandAnnotation(args), ResourceAnnotationMutationBehavior.Replace); | ||
|
|
||
| // Check if the UV environment resource already exists | ||
| var existingResource = builder.ApplicationBuilder.Resources | ||
| .FirstOrDefault(r => string.Equals(r.Name, uvEnvironmentName, StringComparison.OrdinalIgnoreCase)); | ||
| AddInstaller(builder, install); | ||
|
|
||
| // Mark that we're using UV for the Python environment | ||
| builder.WithPythonEnvironment(env => env.Uv = true); | ||
|
|
||
| return builder; | ||
| } | ||
|
|
||
| IResourceBuilder<PythonUvEnvironmentResource> uvBuilder; | ||
| /// <summary> | ||
| /// Adds a UV environment setup task to ensure the virtual environment exists before running the Python application. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of the Python application resource, must derive from <see cref="PythonAppResource"/>.</typeparam> | ||
| /// <param name="builder">The resource builder.</param> | ||
| /// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for method chaining.</returns> | ||
| /// <remarks> | ||
| /// This method is obsolete. Use <see cref="WithUv{T}(IResourceBuilder{T}, bool, string[])"/> instead. | ||
| /// </remarks> | ||
| [Obsolete("Use WithUv instead.")] | ||
| [EditorBrowsable(EditorBrowsableState.Never)] | ||
| public static IResourceBuilder<T> WithUvEnvironment<T>(this IResourceBuilder<T> builder) | ||
| where T : PythonAppResource | ||
| { | ||
| return builder.WithUv(install: true, args: null); | ||
| } | ||
|
|
||
| if (existingResource is not null) | ||
| private static void AddInstaller<T>(IResourceBuilder<T> builder, bool install) where T : PythonAppResource | ||
| { | ||
| // Only install packages if in run mode | ||
| if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) | ||
| { | ||
| // Resource already exists, return a builder for it | ||
| if (existingResource is not PythonUvEnvironmentResource uvEnvironmentResource) | ||
| // Check if the installer resource already exists | ||
| var installerName = $"{builder.Resource.Name}-installer"; | ||
| builder.ApplicationBuilder.TryCreateResourceBuilder<PythonInstallerResource>(installerName, out var existingResource); | ||
|
|
||
| if (!install) | ||
| { | ||
| throw new DistributedApplicationException($"Cannot add UV environment resource with name '{uvEnvironmentName}' because a resource of type '{existingResource.GetType()}' with that name already exists."); | ||
| if (existingResource != null) | ||
| { | ||
| // Remove existing installer resource if install is false | ||
| builder.ApplicationBuilder.Resources.Remove(existingResource.Resource); | ||
| builder.Resource.Annotations.OfType<WaitAnnotation>() | ||
| .Where(w => w.Resource == existingResource.Resource) | ||
| .ToList() | ||
| .ForEach(w => builder.Resource.Annotations.Remove(w)); | ||
| builder.Resource.Annotations.OfType<PythonPackageInstallerAnnotation>() | ||
| .ToList() | ||
| .ForEach(a => builder.Resource.Annotations.Remove(a)); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| uvBuilder = builder.ApplicationBuilder.CreateResourceBuilder(uvEnvironmentResource); | ||
| } | ||
| else | ||
| { | ||
| // Resource doesn't exist, create it | ||
| var uvEnvironmentResource = new PythonUvEnvironmentResource(uvEnvironmentName, builder.Resource); | ||
| if (existingResource is not null) | ||
| { | ||
| // Installer already exists, it will be reconfigured via BeforeStartEvent | ||
| return; | ||
| } | ||
|
|
||
| uvBuilder = builder.ApplicationBuilder.AddResource(uvEnvironmentResource) | ||
| .WithArgs("sync") | ||
| .WithParentRelationship(builder) | ||
| var installer = new PythonInstallerResource(installerName, builder.Resource); | ||
| var installerBuilder = builder.ApplicationBuilder.AddResource(installer) | ||
| .WithParentRelationship(builder.Resource) | ||
| .ExcludeFromManifest(); | ||
|
|
||
| builder.WaitForCompletion(uvBuilder) | ||
| .WithPythonEnvironment(env => env.Uv = true); | ||
| } | ||
| builder.ApplicationBuilder.Eventing.Subscribe<BeforeStartEvent>((_, _) => | ||
| { | ||
| // Set the installer's working directory to match the resource's working directory | ||
| // and set the install command and args based on the resource's annotations | ||
| if (!builder.Resource.TryGetLastAnnotation<PythonPackageManagerAnnotation>(out var packageManager) || | ||
| !builder.Resource.TryGetLastAnnotation<PythonInstallCommandAnnotation>(out var installCommand)) | ||
| { | ||
| throw new InvalidOperationException("PythonPackageManagerAnnotation and PythonInstallCommandAnnotation are required when installing packages."); | ||
| } | ||
|
|
||
| return builder; | ||
| installerBuilder | ||
| .WithCommand(packageManager.ExecutableName) | ||
| .WithWorkingDirectory(builder.Resource.WorkingDirectory) | ||
| .WithArgs(installCommand.Args); | ||
|
|
||
| return Task.CompletedTask; | ||
| }); | ||
|
Comment on lines
+1299
to
+1317
|
||
|
|
||
| // Make the parent resource wait for the installer to complete | ||
| builder.WaitForCompletion(installerBuilder); | ||
|
|
||
| builder.WithAnnotation(new PythonPackageInstallerAnnotation(installer)); | ||
| } | ||
| } | ||
|
|
||
| internal static IResourceBuilder<PythonAppResource> WithPythonEnvironment(this IResourceBuilder<PythonAppResource> builder, Action<PythonEnvironmentAnnotation> configure) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
|
|
||
| namespace Aspire.Hosting.Python; | ||
|
|
||
| /// <summary> | ||
| /// Represents the annotation for the Python package manager's install command. | ||
| /// </summary> | ||
| /// <param name="args"> | ||
| /// The command line arguments for the Python package manager's install command. | ||
| /// This includes the command itself (i.e. "install", "sync"). | ||
| /// </param> | ||
| internal sealed class PythonInstallCommandAnnotation(string[] args) : IResourceAnnotation | ||
| { | ||
| /// <summary> | ||
| /// Gets the command-line arguments supplied to the Python package manager. | ||
| /// </summary> | ||
| public string[] Args { get; } = args; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
|
|
||
| namespace Aspire.Hosting.Python; | ||
|
|
||
| /// <summary> | ||
| /// Represents an annotation for a Python installer resource. | ||
| /// </summary> | ||
| internal sealed class PythonPackageInstallerAnnotation(ExecutableResource installerResource) : IResourceAnnotation | ||
| { | ||
| /// <summary> | ||
| /// The instance of the Installer resource used. | ||
| /// </summary> | ||
| public ExecutableResource Resource { get; } = installerResource; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using Aspire.Hosting.ApplicationModel; | ||
|
|
||
| namespace Aspire.Hosting.Python; | ||
|
|
||
| /// <summary> | ||
| /// Represents the annotation for the Python package manager used in a resource. | ||
| /// </summary> | ||
| /// <param name="executableName">The name of the executable used to run the package manager.</param> | ||
| internal sealed class PythonPackageManagerAnnotation(string executableName) : IResourceAnnotation | ||
| { | ||
| /// <summary> | ||
| /// Gets the executable used to run the Python package manager. | ||
| /// </summary> | ||
| public string ExecutableName { get; } = executableName; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.