Types/OpenPackage.Source/Python.ps1

<#
.SYNOPSIS
    Gets a python package
.DESCRIPTION
    Gets a python package as an open package
#>

param(
# A python package.
# This can be the name of the package in PyPi or a package link from `PyPi.org`
[Parameter(ValueFromPipelineByPropertyName)]
[Alias('PyPi', 'Pip')]
[string[]]
$PythonPackage,

# A list of file wildcards to include.
[Parameter(ValueFromPipelineByPropertyName)]
[SupportsWildcards()]
[string[]]
$Include,

# A list of file wildcards to exclude.
[Parameter(ValueFromPipelineByPropertyName)]
[SupportsWildcards()]
[string[]]
$Exclude,

# The base path within the package.
# Content should be added beneath this base path.
[string]
$BasePath = '/',

# A content type map.
# This maps extensions and URIs to a content type.
[Collections.IDictionary]
$TypeMap = $(
    ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap
),

# The compression option.
[IO.Packaging.CompressionOption]
[Alias('CompressionLevel')]
$CompressionOption = 'Superfast',
        
# If set, will force the redownload of various resources and remove existing files or directories
[switch]
$Force,

# If set, will include hidden files and folders, except for files beneath `.git`
[Alias('IncludeDotFiles')]
[switch]
$IncludeHidden,

# If set, will include the `.git` directory contents if found.
# By default, this content will be excluded.
[Alias('IncludeGitFile','IncludeGitFiles','IncludeGitDirectory')]
[switch]
$IncludeGit,

# If set, will include any content found in `/node_modules`.
# By default, this content will be excluded.
[Alias('IncludeNodeModules')]
[switch]
$IncludeNodeModule,

# If set, will include any content found in `/_site`.
# By default, this content will be excluded.
[Alias('IncludeWebsite')]
[switch]
$IncludeSite,

[string[]]
$PythonPackageIndexDomain = @(
    'pypi.org'
    'www.pypi.org'    
)
)

$pipApp = $ExecutionContext.SessionState.InvokeCommand.GetCommand('pip', 'Application')
if (-not $pipApp) { throw "pip not installed or in path"}

# Collect all named parameters to this,
# so we can pass most of them to the next step.
$namedParameters = [Ordered]@{}

foreach ($key in $MyInvocation.MyCommand.Parameters.Keys) {
    $var = $ExecutionContext.SessionState.PSVariable.Get($key)
    if (-not [string]::IsNullOrEmpty($var.value)) {
        $namedParameters[$key] = $var.value
    }
}

# Remove any node package specific parameters.
$namedParameters.Remove('PythonPackage')
$namedParameters.Remove('PythonPackageIndexDomain')

# We want to
if (-not $env:OpenPackagePath) {throw "No Open Package Path"}

# Get our first OpenPackage package path
$myAppData = @($env:OpenPackagePath -split $(
    if ($IsLinux -or $IsMacOS) {
        ':'
    } else {
        ';'
    }
))[0]

# and put all python packages beneathh this location.
$pythonPackageRoot = Join-Path $myAppData "python"

# If we do not have this directory already, create it
if (-not (Test-Path $pythonPackageRoot)) {
    $null = New-Item -ItemType Directory -Path $pythonPackageRoot -Force 
}

# Push into this location
Push-Location $pythonPackageRoot

# Go over each package we want to get.
foreach ($pythonPack in $PythonPackage) {
    # Packages can be in direct named form, or in a url
    $pythonPackUri = $pythonPack -as [uri]
    # If the url was absolute
    if ($pythonPackUri.IsAbsoluteUri) {
        # check it against possible domain names.
        if ($pythonPackUri.DnsSafeHost -notin $PythonPackageIndexDomain) {            
            Write-Error "Can only use an absolute url from $PythonPackageIndexDomain"
            continue        
        }
        # If the url was versioned
        if ($pythonPackUri.Segments[-1] -match '^\d+\.') {
            $pythonPack = $pythonPackUri.Segments[-2,-1] -replace '/' -join '=='
        } else {
            # Otherwise, split off the package segment of the url
            $pythonPack = 
                $pythonPackUri.Segments[2] -replace '/'
        }
    }
    # `pip` will download a package as .whl
    # (with a number of messages on standard error)
    $pipOutput = @(& $pipApp download '-d' $pythonPackageRoot $pythonPack *>&1) 
    # some of these may actually be errors, so create a collection.
    $pipErrors = @()
    # and make sure we track if the package has been downloaded
    # (for it will actually provide the name twice)
    $packageDownloaded = $false
    # Go over each output.
    :gotPackage foreach ($pipOut in $pipOutput) {
        # If any of the output is an error record
        if ($pipOut -is [Management.Automation.ErrorRecord]) {
            # add it to our list
            $pipErrors += $pipOut
        }
        # If the output is not .whl
        if ($pipOut -notmatch '\S+\.whl$') {
            continue # continue
        }
        # Get the file path from our match
        $whlFile = $matches.0
        # If the file does not exist
        if (-not (Test-Path $whlFile)) {
            # output our errors
            $pipErrors
        } else {
            # Otherwise, get the whl file as an open package.
            $pipPackage = Get-OpenPackage -FilePath $whlFile @namedParameters
            # If the output was a package
            if ($pipPackage -is [IO.Packaging.Package]) {
                # mark that we've downloaded it,
                $packageDownloaded = $true
                $pipPackage # emit the package,
                break gotPackage # and take a break.
            }            
        }        
    }
    # If the packge was not downloaded, output errors.
    if (-not $packageDownloaded) {
        Write-Error $(
            $pipErrors -replace '^ERROR:\s' -join [Environment]::NewLine
        )
        continue        
    }
}
Pop-Location