tasks/python.tasks.ps1

$SkipInstallPythonPoetry = $false
$SkipInitialisePythonPoetry = $false
$SkipRunFlake8 = $false
$SkipRunPyTest = $false
$SkipBuildPythonPackages = $false
$SkipPublishPythonPackages = $false

$PythonPoetryProject = ""
$PythonPublishUser = "user"
$PythonRepositoryName = "ci-python-feed"
$PythonPackageRepoUrl = ""      # e.g. https://pkgs.dev.azure.com/myOrg/Project/_packaging/myfeed/pypi/upload
$PythonPackagePreReleaseTag = $null
$PythonPackagesFilenameFilter = "*.whl"

$PoetryPath = Get-Command poetry -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path

$PyTestResultsPath = Join-Path $here "pytest-test-results.xml"
$PythonCoverageReportPath = Join-Path $CoverageDir "coverage.xml"

task EnsurePython -If { $PythonPoetryProject -ne "" } {

    if (!(Get-Command python -ErrorAction SilentlyContinue)) {
        throw "A Python installation could not be found. Please install Python and ensure it is available on the PATH environment variable."
    }
}

task InstallPythonPoetry -If { !$SkipInstallPythonPoetry } EnsurePython,{

    $existingPoetry = Get-Command poetry -ErrorAction SilentlyContinue
    if (!$existingPoetry -and !$PoetryPath) {       
        # The install script will honour this environment variable. If not explicitly set, we set it to:
        # - On build servers we install within the working directory to ensure it's part of the build agent caching
        # - Otherwise, we install to the user profile directory in a cross-platform way
        $env:POETRY_HOME ??= $IsRunningOnBuildServer ? (Join-Path $here ".poetry") : (Join-Path ($IsWindows ? $env:USERPROFILE : $env:HOME) ".poetry")
        $poetryBinPath = Join-Path $env:POETRY_HOME "bin"

        # If the poetry binary is not found, install it
        if (!(Test-Path (Join-Path $poetryBinPath "poetry"))) {
            Write-Build White "Installing Poetry $env:POETRY_VERSION: $env:POETRY_HOME"
            Invoke-WebRequest -Uri https://install.python-poetry.org/ -OutFile get-poetry.py
            exec { & python get-poetry.py --yes }
            Remove-Item get-poetry.py -Force
        }
        
        # Ensure the poetry tool is avaliable to the rest of the build process
        $script:PoetryPath = Join-Path $poetryBinPath "poetry"
        Write-Build Green "Poetry now available: $PoetryPath"
        if ($poetryBinPath -notin ($env:PATH -split [System.IO.Path]::PathSeparator)) {
            Write-Build White "Adding Poetry to PATH: $poetryBinPath"
            $env:PATH = "$poetryBinPath{0}$env:PATH" -f [System.IO.Path]::PathSeparator
        }
    }
    else {
        if (!$PoetryPath) {
            # Ensure $PoetryPath is set if poetry was already available in the PATH
            $script:PoetryPath = $existingPoetry.Path
        }
        Write-Build Green "Poetry already installed: $PoetryPath"
    }
}

task UpdatePoetryLockfile -If { !$IsRunningOnBuildServer } InstallPythonPoetry,{
    Write-Build White "Ensuring poetry.lock is up-to-date - no packages will be updated"
    Push-Location $PythonPoetryProject
    exec { & $script:PoetryPath lock --no-update }
}

task InitialisePythonPoetry -If { $PythonPoetryProject -ne "" -and !$SkipInitialisePythonPoetry } InstallPythonPoetry,UpdatePoetryLockfile,{
    if (!(Test-Path (Join-Path $PythonPoetryProject "pyproject.toml"))) {
        throw "pyproject.toml not found in $PythonPoetryProject"
    }

    # Default to using virtual environments in the project directory, unless already set
    $env:POETRY_VIRTUALENVS_IN_PROJECT ??= "true"

    # Define the global poetry arguments we will use for all poetry commands
    $script:poetryGlobalArgs = @(
        "--no-interaction"
        "--directory=$PythonPoetryProject"
        "-v"
    )
    Write-Build White "poetryGlobalArgs: $poetryGlobalArgs"

    exec { & $script:PoetryPath install @poetryGlobalArgs --with dev,test }
}

task RunFlake8 -If { $PythonPoetryProject -ne "" -and !$SkipRunFlake8 } InitialisePythonPoetry,{
    Write-Build White "Running flake8"
    # Explicitly change directory as Flake8 does not run when Poetry has the '--directory' argument
    Set-Location $PythonPoetryProject
    exec { & $script:PoetryPath run --no-interaction -v flake8 src -vv }
}

task RunPyTests -If { $PythonPoetryProject -ne "" -and !$SkipRunPyTest } InitialisePythonPoetry,{
    # Explicity change directory as PyTest does not run when Poetry has the '--directory' argument
    Set-Location $PythonPoetryProject
    exec {
        & $script:PoetryPath run --no-interaction -v `
            pytest `
            --cov=src `
            --cov-report=xml:$PythonCoverageReportPath `
            --cov-report=term-missing `
            --junitxml=$PyTestResultsPath
    }
}

task BuildPythonPackages -If { $PythonPoetryProject -ne "" -and !$SkipBuildPythonPackages } Version,EnsurePackagesDir,InitialisePythonPoetry,{
    if (Test-Path (Join-Path $PythonPoetryProject "dist")) {
        Remove-Item (Join-Path $PythonPoetryProject "dist") -Recurse -Force
    }

    # Apply python pre-release versioning conventions
    # Ideally the repo will have a GitVersion configuration such that:
    # - The master/main branch has a pre-release tag of 'rc' (release candidate)
    # - All other branches have a pre-release tag of 'b' (beta)
    # - Tagged commits have no pre-release tag

    # However, as a fallback we must ensure that we always have a PEP440 compliant pre-release tag
    # even if GitVersion does not
    $safePreReleaseLabel = Get-PythonPackagePreReleaseLabelFromSemVer -PreReleaseLabel $GitVersion.PreReleaseLabel

    $PythonPackagePreReleaseTag ??= "{0}{1}" -f $safePreReleaseLabel, $GitVersion.PreReleaseNumber
    $pythonPackageVersion = "$($GitVersion.MajorMinorPatch)$PythonPackagePreReleaseTag"

    Write-Build White "Building Python packages with version: $pythonPackageVersion"
    exec { & $script:PoetryPath version @poetryGlobalArgs $pythonPackageVersion }
    # Make the Python package version available to the rest of the build, since it could be different to the GitVersion
    Set-BuildServerVariable -Name "PythonPackageVersion" -Value $pythonPackageVersion

    # Build the package(s)
    exec { & $script:PoetryPath build @poetryGlobalArgs }

    # Move the .whl files to the standard folder used by other parts of the build
    Copy-Item -Path (Join-Path $PythonPoetryProject "dist" $PythonPackagesFilenameFilter) `
              -Destination $PackagesDir/ `
              -Force `
              -Verbose
}

task PublishPythonPackages -If { $PythonPoetryProject -ne "" -and !$SkipPublishPythonPackages } InitialisePythonPoetry,{
    if (!$PythonPackageRepoUrl) {
        Write-Warning "PythonPackageRepoUrl build variable not set, skipping publish"
    }
    elseif (!$env:PYTHON_PACKAGE_REPOSITORY_KEY) {
        Write-Warning "PYTHON_PACKAGE_REPOSITORY_KEY environment variable not set, skipping publish"
    }
    else {
        exec {
            Write-Build White "Registering Python repository $PythonRepositoryName -> $PythonPackageRepoUrl"
            & $script:PoetryPath config @poetryGlobalArgs repositories.$PythonRepositoryName $PythonPackageRepoUrl
        }

        exec {
            Write-Build White "Publishing Python packages to $PythonRepositoryName"
            & $script:PoetryPath publish @poetryGlobalArgs -u $PythonPublishUser -p $env:PYTHON_PACKAGE_REPOSITORY_KEY -r $PythonRepositoryName
        }
    }
}

task BuildPython  -If { $PythonPoetryProject -ne "" } InitialisePythonPoetry,RunFlake8