File: PythonLanguageSupport.cs
Web Access
Project: src\src\Aspire.Hosting.CodeGeneration.Python\Aspire.Hosting.CodeGeneration.Python.csproj (Aspire.Hosting.CodeGeneration.Python)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.InteropServices;
using Aspire.Hosting.Ats;
 
namespace Aspire.Hosting.CodeGeneration.Python;
 
/// <summary>
/// Provides language support for Python AppHosts.
/// Implements scaffolding, detection, and runtime configuration.
/// </summary>
public sealed class PythonLanguageSupport : ILanguageSupport
{
    /// <summary>
    /// The language/runtime identifier for Python.
    /// </summary>
    private const string LanguageId = "python";
 
    /// <summary>
    /// The code generation target language. This maps to the ICodeGenerator.Language property.
    /// </summary>
    private const string CodeGenTarget = "Python";
 
    private const string LanguageDisplayName = "Python";
    private static readonly string[] s_detectionPatterns = ["apphost.py"];
 
    /// <inheritdoc />
    public string Language => LanguageId;
 
    /// <inheritdoc />
    public Dictionary<string, string> Scaffold(ScaffoldRequest request)
    {
        var files = new Dictionary<string, string>();
 
        // Create apphost.py
        files["apphost.py"] = """
            # Aspire Python AppHost
            # For more information, see: https://aspire.dev
 
            import sys
            from pathlib import Path
            sys.path.insert(0, str(Path(__file__).parent / ".modules"))
 
            from aspire import create_builder
 
            builder = create_builder()
 
            # Add your resources here, for example:
            # redis = builder.add_redis("cache")
            # postgres = builder.add_postgres("db")
 
            builder.build().run()
            """;
 
        // Create requirements.txt
        files["requirements.txt"] = """
            # Aspire Python AppHost requirements
            """;
 
        // Create uv-install.py
        files["uv-install.py"] = """
            # Creates a venv and installs dependencies with uv.
            from __future__ import annotations
 
            import os
            import subprocess
            import sys
            from pathlib import Path
 
 
            def run(command: list[str]) -> None:
                result = subprocess.run(command)
                if result.returncode != 0:
                    sys.exit(result.returncode)
 
 
            root = Path(__file__).resolve().parent
            venv_dir = root / ".venv"
            python_path = venv_dir / ("Scripts" if os.name == "nt" else "bin") / (
                "python.exe" if os.name == "nt" else "python"
            )
 
            if not python_path.exists():
                run(["uv", "venv", str(venv_dir)])
 
            run(["uv", "pip", "install", "-r", "requirements.txt", "--python", str(python_path)])
            """;
 
        // Create apphost.run.json with random ports
        // Use PortSeed if provided (for testing), otherwise use random
        var random = request.PortSeed.HasValue
            ? new Random(request.PortSeed.Value)
            : Random.Shared;
 
        var httpsPort = random.Next(10000, 65000);
        var httpPort = random.Next(10000, 65000);
        var otlpPort = random.Next(10000, 65000);
        var resourceServicePort = random.Next(10000, 65000);
 
        files["apphost.run.json"] = $$"""
            {
              "profiles": {
                "https": {
                  "applicationUrl": "https://localhost:{{httpsPort}};http://localhost:{{httpPort}}",
                  "environmentVariables": {
                    "ASPNETCORE_ENVIRONMENT": "Development",
                    "DOTNET_ENVIRONMENT": "Development",
                    "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:{{otlpPort}}",
                    "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:{{resourceServicePort}}"
                  }
                }
              }
            }
            """;
 
        return files;
    }
 
    /// <inheritdoc />
    public DetectionResult Detect(string directoryPath)
    {
        var appHostPath = Path.Combine(directoryPath, "apphost.py");
        if (!File.Exists(appHostPath))
        {
            return DetectionResult.NotFound;
        }
 
        var requirementsPath = Path.Combine(directoryPath, "requirements.txt");
        if (!File.Exists(requirementsPath))
        {
            return DetectionResult.NotFound;
        }
 
        return DetectionResult.Found(LanguageId, "apphost.py");
    }
 
    /// <inheritdoc />
    public RuntimeSpec GetRuntimeSpec()
    {
        return new RuntimeSpec
        {
            Language = LanguageId,
            DisplayName = LanguageDisplayName,
            CodeGenLanguage = CodeGenTarget,
            DetectionPatterns = s_detectionPatterns,
            InstallDependencies = new CommandSpec
            {
                Command = GetPythonCommand(),
                Args = ["uv-install.py"]
            },
            Execute = new CommandSpec
            {
                Command = "uv",
                Args = ["run", "python", "{appHostFile}"]
            }
        };
    }
 
    /// <summary>
    /// Gets the appropriate Python command for the current platform.
    /// On Windows: tries 'python' first, then 'py' (Python launcher)
    /// On Linux/macOS: tries 'python3' first (more specific), then 'python'
    /// </summary>
    private static string GetPythonCommand()
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            // Try 'python' first, then 'py' (Python launcher)
            if (CommandExists("python"))
            {
                return "python";
            }
            return "py";
        }
        else
        {
            // Try 'python3' first (more specific), then 'python'
            if (CommandExists("python3"))
            {
                return "python3";
            }
            return "python";
        }
    }
 
    /// <summary>
    /// Checks if a command exists in the system PATH.
    /// </summary>
    private static bool CommandExists(string command)
    {
        try
        {
            var pathEnv = Environment.GetEnvironmentVariable("PATH");
            if (string.IsNullOrEmpty(pathEnv))
            {
                return false;
            }
 
            var pathSeparator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ';' : ':';
            var paths = pathEnv.Split(pathSeparator, StringSplitOptions.RemoveEmptyEntries);
 
            var extensions = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                ? new[] { ".exe", ".cmd", ".bat", "" }
                : new[] { "" };
 
            foreach (var path in paths)
            {
                foreach (var ext in extensions)
                {
                    var fullPath = Path.Combine(path, command + ext);
                    if (File.Exists(fullPath))
                    {
                        return true;
                    }
                }
            }
            return false;
        }
        catch
        {
            return false;
        }
    }
}