PythonPip3Dsc.psm1

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

using namespace System.Collections.Generic

#region Functions
function Invoke-Process {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ArgumentList
    )

    try {
        $pinfo = New-Object System.Diagnostics.ProcessStartInfo
        $pinfo.FileName = $FilePath
        $pinfo.RedirectStandardError = $true
        $pinfo.RedirectStandardOutput = $true
        $pinfo.UseShellExecute = $false
        $pinfo.WindowStyle = 'Hidden'
        $pinfo.CreateNoWindow = $true
        $pinfo.Arguments = $ArgumentList
        $p = New-Object System.Diagnostics.Process
        $p.StartInfo = $pinfo
        $p.Start() | Out-Null

        $stOut = @()
        # using ReadLine() instead of ReadToEnd() for building array object. ReadToEnd() gave different output than ReadLine() in some cases.
        while (-not $p.StandardOutput.EndOfStream) {
            $stOut += $p.StandardOutput.ReadLine()
        }

        $stErr = @()
        while (-not $p.StandardError.EndOfStream) {
            $stErr += $p.StandardError.ReadLine()
        }

        $result = [pscustomobject]@{
            Title     = ($MyInvocation.MyCommand).Name
            Command   = $FilePath
            Arguments = $ArgumentList
            StdOut    = $stOut
            StdErr    = $stErr
            ExitCode  = $p.ExitCode
        }

        $p.WaitForExit()

        return $result
    } catch {
        Write-Verbose -Message "Error occurred while executing the command: $FilePath $ArgumentList. Error:"
        Write-Verbose -Message $stErr
    }
}

function Get-Pip3Path {
    if ($IsWindows) {
        # Note: When installing 64-bit version, the registry key: HKLM:\SOFTWARE\Wow6432Node\Python\PythonCore\*\InstallPath was empty.
        $userUninstallRegistry = 'HKCU:\SOFTWARE\Python\PythonCore\*\InstallPath'
        $machineUninstallRegistry = 'HKLM:\SOFTWARE\Python\PythonCore\*\InstallPath'
        $installLocationProperty = 'ExecutablePath'

        $pipExe = TryGetRegistryValue -Key $userUninstallRegistry -Property $installLocationProperty
        if ($null -ne $pipExe) {
            $userInstallLocation = Join-Path (Split-Path $pipExe -Parent) 'Scripts\pip3.exe'
            if ($userInstallLocation) {
                return $userInstallLocation
            }
        }

        $pipExe = TryGetRegistryValue -Key $machineUninstallRegistry -Property $installLocationProperty
        if ($null -ne $pipExe) {
            $machineInstallLocation = Join-Path (Split-Path $pipExe -Parent) 'Scripts\pip3.exe'
            if ($machineInstallLocation) {
                return $machineInstallLocation
            }
        }
    } elseif ($IsMacOS) {
        $pipExe = Join-Path '/Library' 'Frameworks' 'Python.framework' 'Versions' 'Current' 'bin' 'python'

        if (Test-Path -Path $pipExe) {
            return $pipExe
        }

        $pipExe = (Get-Command -Name 'pip3' -ErrorAction SilentlyContinue).Source

        if ($pipExe) {
            return $pipExe
        }
    } elseif ($IsLinux) {
        $pipExe = Join-Path '/usr/bin' 'pip3'

        if (Test-Path $pipExe) {
            return $pipExe
        }

        $pipExe = (Get-Command -Name 'pip3' -ErrorAction SilentlyContinue).Source

        if ($pipExe) {
            return $pipExe
        }
    } else {
        throw 'Operating system not supported.'
    }
}

function Assert-Pip3 {
    # Try invoking pip3 help with the alias. If it fails, switch to calling npm.cmd directly.
    # This may occur if npm is installed in the same shell window and the alias is not updated until the shell window is restarted.
    try {
        Invoke-Pip3 -command 'help'
        return
    } catch {}

    if (Test-Path -Path $global:pip3ExePath) {
        $global:usePip3Exe = $true
        Write-Verbose "Calling pip3.exe from install location: $global:usePip3Exe"
    } else {
        throw 'Python is not installed'
    }
}

function Get-PackageNameWithVersion {
    param (
        [Parameter(Mandatory = $true)]
        [string]$PackageName,

        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [string]$Version
    )

    if ($PSBoundParameters.ContainsKey('Version') -and -not ([string]::IsNullOrEmpty($Version))) {
        $packageName = $PackageName + '==' + $Version
    }

    return $packageName
}

function Invoke-Pip3Install {
    param (
        [Parameter()]
        [string]$PackageName,

        [Parameter()]
        [string]$Arguments,

        [Parameter()]
        [string]$Version,

        [Parameter()]
        [switch]$IsUpdate,

        [Parameter()]
        [switch]$DryRun
    )

    $command = [List[string]]::new()
    $command.Add('install')
    $command.Add((Get-PackageNameWithVersion -PackageName $PackageName -Version $Version))
    if ($IsUpdate.IsPresent) {
        $command.Add('--force-reinstall')
    }
    if ($DryRun.IsPresent) {
        $command.Add('--dry-run')
    }

    $command.Add($Arguments)
    Write-Verbose -Message "Executing 'pip' install with command: $command"
    $result = Invoke-Pip3 -command $command

    return $result
}

function Invoke-Pip3Uninstall {
    param (
        [Parameter()]
        [string]$PackageName,

        [Parameter()]
        [string]$Arguments,

        [Parameter()]
        [string]$Version
    )

    $command = [List[string]]::new()
    $command.Add('uninstall')
    $command.Add((Get-PackageNameWithVersion -PackageName $PackageName -Version $Version))
    $command.Add($Arguments)

    # '--yes' is needed to ignore conformation required for uninstalls
    $command.Add('--yes')
    return Invoke-Pip3 -command $command
}

function GetPip3CurrentState {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [hashtable[]] $Package,

        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [hashtable] $Parameters
    )

    # Filter out the installed packages from the parameters because it is not a valid parameter when calling .ToHashTable() in the class for comparison
    if ($Parameters.ContainsKey('InstalledPackages')) {
        $Parameters.Remove('InstalledPackages')
    }

    $out = @{
        Exist             = $false
        PackageName       = $Parameters.PackageName
        Version           = $Parameters.Version
        Arguments         = $Parameters.Arguments
        InstalledPackages = $Package
    }

    foreach ($entry in $Package) {
        if ($entry.PackageName -eq $Parameters.PackageName) {
            Write-Verbose -Message "Package exist: $($entry.name)"
            $out.Exist = $true
            $out.Version = $entry.version

            if ($Parameters.ContainsKey('version') -and $entry.version -ne $Parameters.version) {
                Write-Verbose -Message "Package exist, but version is different: $($entry.version)"
                $out.Exist = $false
            }
        }
    }

    return $out
}

function GetInstalledPip3Packages {
    $Arguments = [List[string]]::new()
    $Arguments.Add('list')
    $Arguments.Add('--format=json')

    if ($global:usePip3Exe) {
        $command = "& '$global:pip3ExePath' " + $Arguments
    } else {
        $command = '& pip3 ' + $Arguments
    }

    $res = Invoke-Expression -Command $command | ConvertFrom-Json

    $result = $res | ForEach-Object {
        @{
            PackageName = $_.name
            Version     = $_.version
        }
    }

    return $result
}

function Invoke-Pip3 {
    param (
        [Parameter(Mandatory)]
        [string]$command
    )

    if ($global:usePip3Exe) {
        return Invoke-Process -FilePath $global:pip3ExePath -ArgumentList $command
    } else {
        return Invoke-Process -FilePath pip3 -ArgumentList $command
    }
}

function TryGetRegistryValue {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Key,

        [Parameter(Mandatory = $true)]
        [string]$Property
    )

    if (Test-Path -Path $Key) {
        try {
            return (Get-ItemProperty -Path $Key | Select-Object -ExpandProperty $Property)
        } catch {
            Write-Verbose "Property `"$($Property)`" could not be found."
        }
    } else {
        Write-Verbose 'Registry key does not exist.'
    }
}

#endregion Functions

$global:usePip3Exe = $false
$global:pip3ExePath = Get-Pip3Path

# Assert once that pip3 is already installed on the system.
Assert-Pip3

#region DSCResources
<#
.SYNOPSIS
    The `Pip3Package` DSC Resource allows you to install, update, and uninstall Python packages using pip3.
 
.PARAMETER SID
    The security identifier. This is a key property and should not be set manually.
 
.PARAMETER Exist
    Indicates whether the package should exist. Defaults to $true.
 
.PARAMETER Package
    The name of the Python package to manage. This is a mandatory property.
 
    For a list of Python packages, see https://pypi.org/.
 
.PARAMETER Version
    The version of the Python package to manage. If not specified, the latest version will be used.
 
.PARAMETER Arguments
    Additional arguments to pass to pip3.
 
.PARAMETER InstalledPackages
    A list of installed packages. This property is not configurable.
 
.EXAMPLE
    PS C:\> Invoke-DscResource -ModuleName PythonPip3Dsc -Name Pip3Package -Method Set -Property @{ Package = 'flask' }
 
    This example installs the Flask package.
 
.EXAMPLE
    PS C:\> Invoke-DscResource -ModuleName PythonPip3Dsc -Name Pip3Package -Method Get -Property @{ Package = 'flask'; Version = '1.1.4' }
 
    This example shows how to get the current state of the Flask package with version. If the version is not found, the latest version will be used if flask is found.
 
.EXAMPLE
    PS C:\> Invoke-DscResource -ModuleName PythonPip3Dsc -Name Pip3Package -Method Get -Property @{ Package = 'django'; Exist = $false }
 
    This example shows how Django can be removed from the system.
#>

[DSCResource()]
class Pip3Package {
    [DscProperty(Key, Mandatory)]
    [string]$PackageName

    [DscProperty()]
    [string]$Version

    [DscProperty()]
    [string]$Arguments

    [DscProperty()]
    [bool] $Exist = $true

    [DscProperty(NotConfigurable)]
    [hashtable[]]$InstalledPackages

    [Pip3Package] Get() {
        $this.InstalledPackages = GetInstalledPip3Packages
        $currentState = GetPip3CurrentState -Package $this.InstalledPackages -Parameters $this.ToHashTable()

        return $currentState
    }

    [bool] Test() {
        $currentState = $this.Get()
        if ($currentState.Exist -ne $this.Exist) {
            return $false
        }

        if (-not ([string]::IsNullOrEmpty($this.Version))) {
            if ($this.Version -ne $currentState.Version) {
                return $false
            }
        }

        return $true
    }

    [void] Set() {
        if ($this.Test()) {
            return
        }

        $currentPackage = $this.InstalledPackages | Where-Object { $_.PackageName -eq $this.PackageName }
        if ($currentPackage -and $currentPackage.Version -ne $this.Version -and $this.Exist) {
            Invoke-Pip3Install -PackageName $this.PackageName -Version $this.Version -Arguments $this.Arguments -IsUpdate
        } elseif ($this.Exist) {
            Invoke-Pip3Install -PackageName $this.PackageName -Version $this.Version -Arguments $this.Arguments
        } else {
            Invoke-Pip3Uninstall -PackageName $this.PackageName -Version $this.Version -Arguments $this.Arguments
        }
    }

    [string] WhatIf() {
        if ($this.Exist) {
            $whatIfState = Invoke-Pip3Install -PackageName $this.PackageName -Version $this.Version -Arguments $this.Arguments -DryRun

            $whatIfResult = $whatIfState.StdOut
            if ($whatIfState.ExitCode -ne 0) {
                $whatIfResult = $whatIfState.StdErr
            }

            $out = @{
                PackageName = $this.PackageName
                _metaData   = @{
                    whatIf = $whatIfResult
                }
            }
        } else {
            # Uninstall does not have --dry-run param
            $out = @{}
        }

        return ($out | ConvertTo-Json -Depth 10 -Compress)
    }

    static [Pip3Package[]] Export() {
        $packages = GetInstalledPip3Packages
        $out = [List[Pip3Package]]::new()
        foreach ($package in $packages) {
            $in = [Pip3Package]@{
                PackageName       = $package.PackageName
                Version           = $package.version
                Exist             = $true
                Arguments         = $null
                InstalledPackages = $packages
            }

            $out.Add($in)
        }

        return $out
    }

    #region Pip3Package Helper functions
    [hashtable] ToHashTable() {
        $parameters = @{}
        foreach ($property in $this.PSObject.Properties) {
            if (-not ([string]::IsNullOrEmpty($property.Value))) {
                $parameters[$property.Name] = $property.Value
            }
        }

        return $parameters
    }
    #endRegion Pip3Package Helper functions
}

#endregion DSCResources

# SIG # Begin signature block
# MIIoRgYJKoZIhvcNAQcCoIIoNzCCKDMCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD0ofYqg1gmFmNY
# Js76fq9ICCtdZkd/inwXut2dEV6yRqCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0
# Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz
# NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo
# DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3
# a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF
# HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy
# 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC
# Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj
# L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp
# h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3
# cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X
# dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL
# E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi
# u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1
# sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq
# 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb
# DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/
# V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGiYwghoiAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIKWWLirAaj9prbe4zgAX5q1w
# Ok8dxGjYgql/zuBJwRrzMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEALVNypXNP9tYDvMcAShz7Y/gqhKRLjUIduslzHRF0ZCjvj50IDvOnyrg/
# cXxlSnp+kAVtB0AG030UBovakpyzNCwv9+Kq1OGPeDuRG+HmjIvTH4diYzMmZoLy
# JujcksVy5CtiddzIue8+nbIykGt2f0JMHFH4KbVhHzJrLQQhhlyOawxsA+7yvg7B
# aNucU2xf2c1NGNtjICZvZC6/3cyNdC4TpjHVSDSEyBdmhA9Ni6ohaZ4DeTNgH2Vi
# hZcuMRjTMNDQbJWyPeEDkvgDz7lkQq4u5hLDBNqO5GCQJ7u6LLmlvGQaE+sOSUtJ
# mGLndNbrgLhd54CshiWh1BJpjMzvTaGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCC
# F5gGCSqGSIb3DQEHAqCCF4kwgheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq
# hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCAO3gIWM4KOcWpvJ/WCJ92Uj82DWwz0TSzIeEuFEEyUNAIGaC4JajF9
# GBMyMDI1MDUyODIwMjc0Ni41ODNaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# Tjo0QzFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaCCEf4wggcoMIIFEKADAgECAhMzAAAB/xI4fPfBZdahAAEAAAH/MA0G
# CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0
# MDcyNTE4MzExOVoXDTI1MTAyMjE4MzExOVowgdMxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w
# ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjRDMUEt
# MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl
# MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyeiV0pB7bg8/qc/mkiDd
# JXnzJWPYgk9mTGeI3pzQpsyrRJREWcKYHd/9db+g3z4dU4VCkAZEXqvkxP5QNTtB
# G5Ipexpph4PhbiJKwvX+US4KkSFhf1wflDAY1tu9CQqhhxfHFV7vhtmqHLCCmDxh
# ZPmCBh9/XfFJQIUwVZR8RtUkgzmN9bmWiYgfX0R+bDAnncUdtp1xjGmCpdBMygk/
# K0h3bUTUzQHb4kPf2ylkKPoWFYn2GNYgWw8PGBUO0vTMKjYD6pLeBP0hZDh5P3f4
# xhGLm6x98xuIQp/RFnzBbgthySXGl+NT1cZAqGyEhT7L0SdR7qQlv5pwDNerbK3Y
# SEDKk3sDh9S60hLJNqP71iHKkG175HAyg6zmE5p3fONr9/fIEpPAlC8YisxXaGX4
# RpDBYVKpGj0FCZwisiZsxm0X9w6ZSk8OOXf8JxTYWIqfRuWzdUir0Z3jiOOtaDq7
# XdypB4gZrhr90KcPTDRwvy60zrQca/1D1J7PQJAJObbiaboi12usV8axtlT/dCeP
# C4ndcFcar1v+fnClhs9u3Fn6LkHDRZfNzhXgLDEwb6dA4y3s6G+gQ35o90j2i6am
# aa8JsV/cCF+iDSGzAxZY1sQ1mrdMmzxfWzXN6sPJMy49tdsWTIgZWVOSS9uUHhSY
# kbgMxnLeiKXeB5MB9QMcOScCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBTD+pXk/rT/
# d7E/0QE7hH0wz+6UYTAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf
# BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz
# L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww
# bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El
# MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF
# BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAOSNN5MpLiyun
# m866frWIi0hdazKNLgRp3WZPfhYgPC3K/DNMzLliYQUAp6WtgolIrativXjOG1lI
# jayG9r6ew4H1n5XZdDfJ12DLjopap5e1iU/Yk0eutPyfOievfbsIzTk/G51+uiUJ
# k772nVzau6hI2KGyGBJOvAbAVFR0g8ppZwLghT4z3mkGZjq/O4Z/PcmVGtjGps2T
# CtI4rZjPNW8O4c/4aJRmYQ/NdW91JRrOXRpyXrTKUPe3kN8N56jpl9kotLhdvd89
# RbOsJNf2XzqbAV7XjV4caCglA2btzDxcyffwXhLu9HMU3dLYTAI91gTNUF7BA9q1
# EvSlCKKlN8N10Y4iU0nyIkfpRxYyAbRyq5QPYPJHGA0Ty0PD83aCt79Ra0IdDIMS
# uwXlpUnyIyxwrDylgfOGyysWBwQ/js249bqQOYPdpyOdgRe8tXdGrgDoBeuVOK+c
# RClXpimNYwr61oZ2/kPMzVrzRUYMkBXe9WqdSezh8tytuulYYcRK95qihF0irQs6
# /WOQJltQX79lzFXE9FFln9Mix0as+C4HPzd+S0bBN3A3XRROwAv016ICuT8hY1In
# yW7jwVmN+OkQ1zei66LrU5RtAz0nTxx5OePyjnTaItTSY4OGuGU1SXaH49JSP3t8
# yGYA/vorbW4VneeD721FgwaJToHFkOIwggdxMIIFWaADAgECAhMzAAAAFcXna54C
# m0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZp
# Y2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMy
# MjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV
# BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0B
# AQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51
# yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY
# 6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9
# cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN
# 7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDua
# Rr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74
# kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2
# K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5
# TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZk
# i1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9Q
# BXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3Pmri
# Lq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUC
# BBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJl
# pxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y
# eS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUA
# YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw
# MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/yp
# b+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulm
# ZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM
# 9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECW
# OKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4
# FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3Uw
# xTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPX
# fx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVX
# VAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGC
# onsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU
# 5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEG
# ahC0HVUzWLOhcGbyoYIDWTCCAkECAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT
# Tjo0QzFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# U2VydmljZaIjCgEBMAcGBSsOAwIaAxUAqROMbMS8JcUlcnPkwRLFRPXFspmggYMw
# gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF
# AAIFAOvhwdIwIhgPMjAyNTA1MjgxNzA5MzhaGA8yMDI1MDUyOTE3MDkzOFowdzA9
# BgorBgEEAYRZCgQBMS8wLTAKAgUA6+HB0gIBADAKAgEAAgIkMQIB/zAHAgEAAgIT
# yDAKAgUA6+MTUgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAow
# CAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQBpjyqg7fkA
# 3iv2nZxUCxM+1lbHy6TS978FF91yo2N/TezzMMheBiVs5ZsOG0FPNCEO9NrSUGHO
# P0lptYoSoKtsWn93mqW1G80qDT1CO2OO9uFaepRXZM5u18Q8tpEtTxZlnAduw47C
# z+bEg+hEoWdHKJjELhVtVK6FxA2K2OqKXtN19fihv4yvLmtVDeIk+mSDPl3O9smq
# VnIp8ZX2RM2I+714qTrVl1hX29e0/6GrSeVVIuFRymtucHmP4j6953AmAJ6Vgf8x
# pC1nt7iqzpWcd6dt3A3TS0OynlOkMt2kdzasZmrlKcaDgr4m4ix0VFxghoWI0JAu
# BiWCIzAaeoqHMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENB
# IDIwMTACEzMAAAH/Ejh898Fl1qEAAQAAAf8wDQYJYIZIAWUDBAIBBQCgggFKMBoG
# CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgYNor2vmf
# 9ToJCbLW0TsQ8WloxDtoJNW11zg0Pddnl7gwgfoGCyqGSIb3DQEJEAIvMYHqMIHn
# MIHkMIG9BCDkMu++yQJ3aaycIuMT6vA7JNuMaVOI3qDjSEV8upyn/TCBmDCBgKR+
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB/xI4fPfBZdahAAEA
# AAH/MCIEIAkCUnv9pjzZGZeW9ZXxjCoVaRz+CF0FPIvD3okKPDo2MA0GCSqGSIb3
# DQEBCwUABIICAL+4j6kVWagu1c9E29lxB61PgyPhqu2YhV/RoYumB+ppvAtqf6sa
# PBRePuMIYRgb6lCWMKyob4TGKb1QuIqjCqCnRU9MyVJIpGoi4sKwimfw6TC3O4Ki
# AccWBuK+8GqA5E7QWfrN+OSIAwBk5h0bsctVjruGjoXuncZyO5I7+mU8DcD7Pc+I
# rmrjqJmq1Uzykae05p4RbnaAIgtteoTr0ttrxVWl7uBSjonHq9O5w58ACPBf69XI
# IRp3HCYy7GK5wL6H/4joPcROH4ZqvaRB9hvIblP/IVFIEUbqiHL1CSzb+rBgPSUf
# KA32yLd/T4r6l/pOmngGv9wO8ZYp12H6z6RKVVN6ch33QxARb3RbXH3ZAwUnmEym
# Ulo+xBcKnLmrdU5uY6NOFZV2J3zT9m9Pec4AfFDvW0LhuaH49POdlyVkEIuSg3NO
# 94dtRLFLPxrfvbnP8WgJMoWbBj5mHIDvZNSzPgZGw6KJ2vWqwExj043E0m2g660u
# UWc51tfiMMCdd8iUkKjY4eMg1xRpEh4SZeBQ0Me4j9AFubDs9PR2r1CPBdiIwYH6
# 9vytjEYGC4K/ykfTbTkgSU3jPUny5B8WhVD03bWmhe4QVixCXA8b6UF6AEj4TmFx
# wVhegHP/ibC9dYzsJPLcLZahncCO3X4rZETAedIRWRd5dy+OlfSa/lpY
# SIG # End signature block