Public/Invoke-Python.ps1

Function Invoke-Python {
    <#
    .SYNOPSIS
    Run a Python Script
 
    .DESCRIPTION
    This function executes a python script
    and returns output in a PowerShell friendly way
 
    .PARAMETER Script
    A ScriptBlock-equivelent.
     
    .PARAMETER ScriptPath
    File Path of a python script file to execute.
 
    .EXAMPLE
       Invoke-Python -Script 'print("Hello, World!")'
 
    .EXAMPLE
       Invoke-Python -Path 'perform_action.py'
     
    .EXAMPLE
       $PythonOutput = Invoke-Python -Path 'perform_action.py' -Passthru
 
    .EXAMPLE
       Invoke-Python -Path 'perform_action.py' -Console
    ## Writes output to console using Write-Host
 
    .OUTPUTS
    Array of Strings output by the script, only when using -Passthru
    #>

    param(
        [CmdletBinding()]
        [Parameter(mandatory = $true, ParameterSetName = 'File')]$ScriptPath,
        [Parameter(mandatory = $true, ParameterSetName = 'Script')][string]$Script,
        [Parameter(mandatory = $false)][String]$PythonPath = 'C:\Python313\python.exe',
        [Parameter(mandatory = $false)][String]$PythonVersion,
        [Parameter(mandatory = $false)][Switch]$PassThru,
        [Parameter(mandatory = $false)][Switch]$Console
    )
    begin {
        if (-Not $PythonPath) {
            if ($IsMacOS) {
                $PythonPath = "/usr/local/bin/python$($PythonVersion)"
            }
        }

        ## Test if PsExec is installed
        $PythonFileExists = Test-Path -Path $PythonPath
        if (-Not $PythonFileExists) {
            Throw "Python is not installed at $($PythonPath). Install there, or use the -PythonPath Parameter to define the copy to use."
        }

        switch ($PSCmdlet.ParameterSetName) {
            'Script' {
                $ScriptPath = "$(New-TemporaryFile).py"
                Set-Content -Path $ScriptPath -Value $Script.ToString() -Force
                break
            }
            'File' {
                ## Test if PsExec is installed
                if (-not (Test-Path -Path $ScriptPath)) {
                    Throw "The file does not exist: $($ScriptPath)"
                }
                break
            }
        }
    }
    
    process {

        try {

            $PythonArguments = "-q"    # don't print version and copyright messages on interactive startup
            $PythonArguments += " -u "    # force the stdout and stderr streams to be unbuffered;
            $PythonArguments += $ScriptPath
        
            ## Setup Standard Process Options
            $PythonProcInfo = New-Object System.Diagnostics.ProcessStartInfo
            $PythonProcInfo.FileName = $PythonPath
            $PythonProcInfo.UseShellExecute = $false
            $PythonProcInfo.CreateNoWindow = $true

            ## Using Normal actually hides the window
            ## Because the CreateNoWindow is True
            $PythonProcInfo.WindowStyle = 'Hidden'

            ## Redirect Standard Streams
            $PythonProcInfo.RedirectStandardError = $true    ## True
            $PythonProcInfo.RedirectStandardOutput = $true   ## True
            $PythonProcInfo.RedirectStandardInput = $true    ## True
            $PythonProcInfo.Arguments = $PythonArguments

            $PythonProc = New-Object System.Diagnostics.Process
            $PythonProc.StartInfo = $PythonProcInfo

            ## Create an Event Handler for Standard Out
            $global:PythonStdOut = [System.Text.StringBuilder]::New()
            $global:PythonStdErr = [System.Text.StringBuilder]::New()

            ## Start the Python Process
            [Void]$PythonProc.Start()

            ## Keep reading Standard Out until the stream has ended
            while (!($PythonProc.StandardOutput.EndOfStream)) {
                $Line = $PythonProc.StandardOutput.ReadLine()
                [void]($global:PythonStdOut.AppendLine($Line))
                if ($Console) {
                    Write-Host $Line
                }
            }

            ## Then read all of Standard Err
            while (!($PythonProc.StandardError.EndOfStream)) {
                $Line = $PythonProc.StandardError.ReadLine()
                [void]($global:PythonStdErr.AppendLine($Line))
                Write-Host $Line
            }

            ## Wait for the Process to Exit
            $PythonProc.WaitForExit()

        }
        catch {
            throw $_
        }
        finally {
            
            $PythonProc.Close()
            $PythonProc.Dispose()
            if ($PSCmdlet.ParameterSetName -eq 'Script') {
                Remove-Item -Path $ScriptPath -Force
            }
        }

        ## Convert the output to a String
        $PythonOut = $global:PythonStdOut.toString().Trim()
        $PythonErr = $global:PythonStdErr.toString().Trim()

        ## Throw on Fatal Errors
        if ($PythonErr) {
            throw $PythonErr
        }
    }
    
    end {
        if ($PassThru) {
            return $PythonOut
        }
    }
}