sqlnotebook.psm1
#------------------------------------------------------------ # Copyright (c) Microsoft Corporation. All rights reserved. #------------------------------------------------------------ Set-StrictMode -Version Latest function Invoke-SqlNotebook { [CmdletBinding(DefaultParameterSetName="ByConnectionParameters")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUsePSCredentialType", "Username", Justification="Intentionally allowing User/Password, in addition to a PSCredential parameter.")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password", Justification="Intentionally allowing User/Password, in addition to a PSCredential parameter.")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUsernameAndPasswordParams", "", Justification="Intentionally allowing User/Password, in addition to a PSCredential parameter.")] # Parameters param( [Parameter(Mandatory = $false, ParameterSetName = 'ByConnectionParameters')]$ServerInstance, [Parameter(Mandatory = $false, ParameterSetName = 'ByConnectionParameters')]$Database, [Parameter(Mandatory = $false, ParameterSetName = 'ByConnectionParameters')][ValidateNotNullorEmpty()]$Username, [Parameter(Mandatory = $false, ParameterSetName = 'ByConnectionParameters')][ValidateNotNullorEmpty()]$Password, [Parameter(Mandatory = $false, ParameterSetName = 'ByConnectionString')][ValidateNotNullorEmpty()]$ConnectionString, [Parameter(Mandatory = $false, ParameterSetName = 'ByConnectionParameters')][ValidateNotNullorEmpty()][PSCredential]$Credential, [Parameter(Mandatory = $true, ParameterSetName='ByInputFile')] [Parameter(ParameterSetName = 'ByConnectionParameters')] [Parameter(ParameterSetName = 'ByConnectionString')]$InputFile, [Parameter(Mandatory = $true, ParameterSetName='ByInputObject')] [Parameter(ParameterSetName = 'ByConnectionParameters')] [Parameter(ParameterSetName = 'ByConnectionString')]$InputObject, [Parameter(Mandatory = $false)][ValidateNotNullorEmpty()]$OutputFile, [Parameter(Mandatory = $false, ParameterSetName = 'ByConnectionParameters')][ValidateNotNullorEmpty()][string]$AccessToken, [Switch]$Force ) #Checks to see if OutputFile is given #If it is, checks to see if extension is there function getOutputFile($inputFile,$outputFile) { if($outputFile) { $extn = [IO.Path]::GetExtension($outputFile) if ($extn.Length -eq 0) { $outputFile = ($outputFile + ".ipynb") } $outputFile } else { #If User does not define Output it will use the inputFile file location $fileinfo = Get-Item $inputFile # Create an output file based on the file path of input and name Join-Path $fileinfo.DirectoryName ($fileinfo.BaseName + "_out" + $fileinfo.Extension) } } #Validates InputFile and Converts InputFile to Json Object function getFileContents($inputfile) { if (-not (Test-Path -Path $inputfile)) { Throw New-Object System.IO.FileNotFoundException ($inputfile + " does not exist") } $fileItem = Get-Item $inputfile #Checking if file is a python notebook if ($fileItem.Extension -ne ".ipynb") { Throw New-Object System.FormatException "Only ipynb files are supported" } $fileContent = Get-Content $inputfile try { $fileContentJson = ($fileContent | ConvertFrom-Json) } catch { Throw New-Object System.FormatException "Malformed Json file" } $fileContentJson } #Validate SQL Kernel Notebook function validateKernelType($fileContentJson) { if ($fileContentJson.metadata.kernelspec.name -ne "SQL") { Throw New-Object System.NotSupportedException "Kernel type '$($fileContentJson.metadata.kernelspec.name)' not supported." } } #Validate non-existing output file #If file exists and $throwifexists, an exception is thrown. function validateExistingOutputFile($outputfile, $throwifexists) { if ($outputfile -and (Test-Path $outputfile) -and $throwifexists) { Throw New-Object System.IO.IOException "The file '$($outputfile)' already exists. Please, specify -Force to overwrite it." } } #Parsing Notebook Data to Notebook Output function ParseTableToNotebookOutput { param ( [System.Data.DataTable] $DataTable, [int] $CellExecutionCount ) $TableHTMLText = "<table>" $TableSchemaFeilds = @() $TableHTMLText += "<tr>" foreach ($ColumnName in $DataTable.Columns) { $TableSchemaFeilds += @(@{name = $ColumnName.toString() }) $TableHTMLText += "<th>" + $ColumnName.toString() + "</th>" } $TableHTMLText += "</tr>" $TableSchema = @{ } $TableSchema["fields"] = $TableSchemaFeilds $TableDataRows = @() foreach ($Row in $DataTable) { $TableDataRow = [ordered]@{ } $TableHTMLText += "<tr>" $i = 0 foreach ($Cell in $Row.ItemArray) { $TableDataRow[$i.ToString()] = $Cell.toString() $TableHTMLText += "<td>" + $Cell.toString() + "</td>" $i++ } $TableHTMLText += "</tr>" $TableDataRows += $TableDataRow } $TableDataResource = @{ } $TableDataResource["schema"] = $TableSchema $TableDataResource["data"] = $TableDataRows $TableData = @{ } $TableData["application/vnd.dataresource+json"] = $TableDataResource $TableData["text/html"] = $TableHTMLText $TableOutput = @{ } $TableOutput["output_type"] = "execute_result" $TableOutput["data"] = $TableData $TableOutput["metadata"] = @{ } $TableOutput["execution_count"] = $CellExecutionCount return $TableOutput } #Parsing the Error Messages to Notebook Output function ParseQueryErrorToNotebookOutput { param ( $QueryError ) <# Following the current syntax of errors in T-SQL notebooks from ADS #> $ErrorString = "Msg " + $QueryError.Exception.InnerException.Number + ", Level " + $QueryError.Exception.InnerException.Class + ", State " + $QueryError.Exception.InnerException.State + ", Line " + $QueryError.Exception.InnerException.LineNumber + "`r`n" + $QueryError.Exception.Message $ErrorOutput = @{ } $ErrorOutput["output_type"] = "error" $ErrorOutput["traceback"] = @() $ErrorOutput["evalue"] = $ErrorString return $ErrorOutput } #Parsing Messages to Notebook Output function ParseStringToNotebookOutput { param ( [System.String] $InputString ) <# Parsing the string to notebook cell output. It's the standard Jupyter Syntax #> $StringOutputData = @{ } $StringOutputData["text/html"] = $InputString $StringOutput = @{ } $StringOutput["output_type"] = "display_data" $StringOutput["data"] = $StringOutputData $StringOutput["metadata"] = @{ } return $StringOutput } #Start of Script #Checks to see if InputFile or InputObject was entered #Checks to InputFile Type and initializes OutputFile if ($InputFile -is [System.String]) { $fileInformation = getFileContents($InputFile) $fileContent = $fileInformation[0] $OutputFile = getOutputFile $InputFile $OutputFile } elseif ($InputFile -is [System.IO.FileInfo]) { $fileInformation = getFileContents($InputFile.FullName) $fileContent = $fileInformation[0] $OutputFile = getOutputFile $InputFile $OutputFile } else { $fileContent = $InputObject } #Checks InputObject and converts that to appropriate Json object if ($InputObject -is [System.String]) { $fileContentJson = ($InputObject | ConvertFrom-Json) $fileContent = $fileContentJson[0] } #Validates only SQL Notebooks validateKernelType $fileContent #Validate that $OutputFile does not exist, or, if it exists a -Force was passed in. validateExistingOutputFile $OutputFile (-not $Force) #Setting params for Invoke-Sqlcmd $DatabaseQueryHashTable = @{ } #Checks to see if User entered ConnectionString or individual parameters if ($ConnectionString) { $DatabaseQueryHashTable["ConnectionString"] = $ConnectionString } else { if ($ServerInstance) { $DatabaseQueryHashTable["ServerInstance"] = $ServerInstance } if ($Database) { $DatabaseQueryHashTable["Database"] = $Database } #Checks to see if User entered AccessToken, Credential, or individual parameters if ($AccessToken) { $DatabaseQueryHashTable["AccessToken"] = $AccessToken } else { if ($Credential) { $DatabaseQueryHashTable["Credential"] = $Credential } else { if ($Username) { $DatabaseQueryHashTable["Username"] = $Username } if ($Password) { $DatabaseQueryHashTable["Password"] = $Password } } } } #Setting additional parameters for Invoke-SQLCMD to get #all the information from Notebook execution to output $DatabaseQueryHashTable["Verbose"] = $true $DatabaseQueryHashTable["ErrorVariable"] = "SqlQueryError" $DatabaseQueryHashTable["OutputAs"] = "DataTables" #The first code cell number $cellExecutionCount = 1 #Iterate through Notebook Cells $fileContent.cells | Where-Object { # Ignoring Markdown or raw cells $_.cell_type -ne "markdown" -and $_.cell_type -ne "raw" -and $_.source -ne "" } | ForEach-Object { $NotebookCellOutputs = @() # Getting the source T-SQL from the cell # Note that the cell's source field can be # an array (or strings) or a scalar (string). # If an array, elements are properly terminated with CR/LF. $DatabaseQueryHashTable["Query"] = $_.source -join '' # Executing the T-SQL Query and storing the result and the time taken to execute $SqlQueryExecutionTime = Measure-Command { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "SqlQueryResult", Justification="Suppressing false warning.")] $SqlQueryResult = @( Invoke-Sqlcmd @DatabaseQueryHashTable -ErrorAction SilentlyContinue 4>&1) } # Setting the Notebook Cell Execution Count to increase count of each code cell # Note: handle the case where the 'execution_count' property is missing. if (-not ($_ | Get-Member execution_count)) { $_ | Add-Member -Name execution_count -Value $null -MemberType NoteProperty } $_.execution_count = $cellExecutionCount++ $NotebookCellTableOutputs = @() <# Iterating over the results by Invoke-Sqlcmd There are 2 types of errors: 1. Verbose Output: Print Statements: These needs to be added to the beginning of the cell outputs 2. Datatables from the database These needs to be added to the end of cell outputs #> $SqlQueryResult | ForEach-Object { if ($_ -is [System.Management.Automation.VerboseRecord]) { # Adding the print statments to the cell outputs $NotebookCellOutputs += $(ParseStringToNotebookOutput($_.Message)) } elseif ($_ -is [System.Data.DataTable]) { # Storing the print Tables into an array to be added later to the cell output $NotebookCellTableOutputs += $(ParseTableToNotebookOutput $_ $CellExecutionCount) } elseif ($_ -is [System.Data.DataRow]) { # Storing the print row into an array to be added later to the cell output $NotebookCellTableOutputs += $(ParseTableToNotebookOutput $_.Table $CellExecutionCount) } } if ($SqlQueryError) { # Adding the parsed query error from Invoke-Sqlcmd $NotebookCellOutputs += $(ParseQueryErrorToNotebookOutput($SqlQueryError)) } if ($SqlQueryExecutionTime) { # Adding the parsed execution time from Measure-Command $NotebookCellExcutionTimeString = "Total execution time: " + $SqlQueryExecutionTime.ToString() $NotebookCellOutputs += $(ParseStringToNotebookOutput($NotebookCellExcutionTimeString)) } # Adding the data tables $NotebookCellOutputs += $NotebookCellTableOutputs # In the unlikely case the 'outputs' property is missing from the JSON # object, we add it. if (-not ($_ | Get-Member outputs)) { $_ | Add-Member -Name outputs -Value $null -MemberType NoteProperty } $_.outputs = $NotebookCellOutputs } # This will update the Output file according to the executed output of the notebook if ($OutputFile) { ($fileContent | ConvertTo-Json -Depth 100 ) | Out-File -Encoding Ascii -FilePath $OutputFile Get-Item $OutputFile } else { $fileContent | ConvertTo-Json -Depth 100 } } # SIG # Begin signature block # MIIjvQYJKoZIhvcNAQcCoIIjrjCCI6oCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDa0g1ksj3mus05 # CXhKi3QwcyakVU5s4+QzqXPKyPwzyaCCDYEwggX/MIID56ADAgECAhMzAAABh3IX # chVZQMcJAAAAAAGHMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjAwMzA0MTgzOTQ3WhcNMjEwMzAzMTgzOTQ3WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDOt8kLc7P3T7MKIhouYHewMFmnq8Ayu7FOhZCQabVwBp2VS4WyB2Qe4TQBT8aB # znANDEPjHKNdPT8Xz5cNali6XHefS8i/WXtF0vSsP8NEv6mBHuA2p1fw2wB/F0dH # sJ3GfZ5c0sPJjklsiYqPw59xJ54kM91IOgiO2OUzjNAljPibjCWfH7UzQ1TPHc4d # weils8GEIrbBRb7IWwiObL12jWT4Yh71NQgvJ9Fn6+UhD9x2uk3dLj84vwt1NuFQ # itKJxIV0fVsRNR3abQVOLqpDugbr0SzNL6o8xzOHL5OXiGGwg6ekiXA1/2XXY7yV # Fc39tledDtZjSjNbex1zzwSXAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUhov4ZyO96axkJdMjpzu2zVXOJcsw # UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 # ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDU4Mzg1MB8GA1UdIwQYMBaAFEhu # ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w # Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx # MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAixmy # S6E6vprWD9KFNIB9G5zyMuIjZAOuUJ1EK/Vlg6Fb3ZHXjjUwATKIcXbFuFC6Wr4K # NrU4DY/sBVqmab5AC/je3bpUpjtxpEyqUqtPc30wEg/rO9vmKmqKoLPT37svc2NV # BmGNl+85qO4fV/w7Cx7J0Bbqk19KcRNdjt6eKoTnTPHBHlVHQIHZpMxacbFOAkJr # qAVkYZdz7ikNXTxV+GRb36tC4ByMNxE2DF7vFdvaiZP0CVZ5ByJ2gAhXMdK9+usx # zVk913qKde1OAuWdv+rndqkAIm8fUlRnr4saSCg7cIbUwCCf116wUJ7EuJDg0vHe # yhnCeHnBbyH3RZkHEi2ofmfgnFISJZDdMAeVZGVOh20Jp50XBzqokpPzeZ6zc1/g # yILNyiVgE+RPkjnUQshd1f1PMgn3tns2Cz7bJiVUaqEO3n9qRFgy5JuLae6UweGf # AeOo3dgLZxikKzYs3hDMaEtJq8IP71cX7QXe6lnMmXU/Hdfz2p897Zd+kU+vZvKI # 3cwLfuVQgK2RZ2z+Kc3K3dRPz2rXycK5XCuRZmvGab/WbrZiC7wJQapgBodltMI5 # GMdFrBg9IeF7/rP4EqVQXeKtevTlZXjpuNhhjuR+2DMt/dWufjXpiW91bo3aH6Ea # jOALXmoxgltCp1K7hrS6gmsvj94cLRf50QQ4U8Qwggd6MIIFYqADAgECAgphDpDS # AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK # V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0 # ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla # MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT # H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG # OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S # 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz # y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7 # 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u # M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33 # X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl # XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP # 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB # l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF # RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM # CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ # BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud # DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO # 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0 # LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p # Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw # cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA # XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY # 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj # 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd # d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ # Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf # wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ # aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j # NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B # xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96 # eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7 # r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I # RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVkjCCFY4CAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAYdyF3IVWUDHCQAAAAABhzAN # BglghkgBZQMEAgEFAKCB2jAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQg6FU+HA74 # YQRLN+aT+ijOjTgRNcojcXwpU2K6sOnHu04wbgYKKwYBBAGCNwIBDDFgMF6gOoA4 # AFMAUQBMACAAUwBlAHIAdgBlAHIAIABNAGEAbgBhAGcAZQBtAGUAbgB0ACAAUwB0 # AHUAZABpAG+hIIAeaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3NxbC8gMA0GCSqG # SIb3DQEBAQUABIIBAHarkJXrAYOV4PNyV5YbdC9I2L9/ooqHQsLt0parTeRG14ve # dAa4anlZCgvbbyOKVykf73JtS5p3+Wdcuu1Q/o5Ush1tMZ1dJ+E5Gm6rpgq/3ogO # pBiQ0bEukWdtNJfu4RdYm4wD1tvvRxHjyA5E8l+zDF19Uytdq/EPdTzd2bFQS3Vg # c+qNplzYUQIiJT+8S2Unshp0mLSl0Nn9oziNu/3NKXkWOuUp7KdjVqg24rY6wm3b # YQyuPS7RPwEQ1TBmPttOU7ghK65986a3qzlQ+HRK0v11EB+dZyxot+OQRa7HKz5s # 1ATbrytQYP4Bdjux0ssq+HocpyJUoN+/jMa/RHuhghLwMIIS7AYKKwYBBAGCNwMD # ATGCEtwwghLYBgkqhkiG9w0BBwKgghLJMIISxQIBAzEPMA0GCWCGSAFlAwQCAQUA # MIIBVAYLKoZIhvcNAQkQAQSgggFDBIIBPzCCATsCAQEGCisGAQQBhFkKAwEwMTAN # BglghkgBZQMEAgEFAAQgPtuKeG/nSQq7zcvICvuBQ5jNIsM+KNqIT5wn9dHLtm0C # Bl/bmAP+YhgSMjAyMTAxMTQxMzI1NTQuMzdaMASAAgH0oIHUpIHRMIHOMQswCQYD # VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe # MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQLEyBNaWNyb3Nv # ZnQgT3BlcmF0aW9ucyBQdWVydG8gUmljbzEmMCQGA1UECxMdVGhhbGVzIFRTUyBF # U046RDlERS1FMzlBLTQzRkUxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1w # IFNlcnZpY2Wggg5EMIIE9TCCA92gAwIBAgITMwAAAS0uTUHKY2UzoAAAAAABLTAN # BgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0x # OTEyMTkwMTE1MDRaFw0yMTAzMTcwMTE1MDRaMIHOMQswCQYDVQQGEwJVUzETMBEG # A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj # cm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQLEyBNaWNyb3NvZnQgT3BlcmF0aW9u # cyBQdWVydG8gUmljbzEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046RDlERS1FMzlB # LTQzRkUxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggEi # MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpYY30dUk6mEU0t7NIuIyHEjFf # huDdKGIMJUCvqZeA1TBZq9Yk2RxS4907e1ehfjPwV8dIfYtLrtcgZ6gqjTpLiREU # 21ZzOLtyb0cc7EHwowX6g/wjcKDDXpKMpSAc8T+dEpI6daT7J0ASh2gj/LYLr2Fc # 6E0OeKtlaWBD//jmE0HhD6lhYvFoIL7HJLnq3FBpIWFjPA1f+CVOzf62w67WpmG3 # vC7ZFYk0GG4oFEggKK/Q4bQGb6vANAO91xR9nX9sA5S7QJygnLFb10pmd+WwKp3j # eLhEFcvDUHUXhiNbSOlMaAu154xryuDHA3SoWrzSewwJ0j+fhvw05HVg/pTfAgMB # AAGjggEbMIIBFzAdBgNVHQ4EFgQU2WxkfEIBIfhODor/L0O+NPKdhs0wHwYDVR0j # BBgwFoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0 # cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGltU3Rh # UENBXzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+ # aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1TdGFQQ0Ff # MjAxMC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcD # CDANBgkqhkiG9w0BAQsFAAOCAQEAfWsDZPdOq3nAbqDuxM6uwfFBrvRKBV4i/GnN # G27GQwWPc/96fGrVWUQsRzeN2t1hmwjGaCmMUgR+lApHA3MFyHzShG1TM5xdZo6U # BBm6oUfqzzah12aLUlfr5/OQMZnnwDN23C7fljQaRLjmWeJD2VXBbTKOGMkI8aDU # T4sJqfgdB5IULP5f1SINFyWOpORUShPyRRHFWONuejXalGft46Lt2+DgJLRNrq6+ # FelUcNX33zpcWW/DMrxOZqr01STkrVcQrobqoNayHvJWtYGeYoriMlnn7TjXzMNJ # 0mXIRi4oA3iJ8Ol38MIBZwuUJfD239ozsQlJbgGG88pCPjJwYjCCBnEwggRZoAMC # AQICCmEJgSoAAAAAAAIwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENl # cnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTEwMDcwMTIxMzY1NVoXDTI1MDcw # MTIxNDY1NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO # BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEm # MCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggEiMA0GCSqG # SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpHQ28dxGKOiDs/BOX9fp/aZRrdFQQ1aUK # AIKF++18aEssX8XD5WHCdrc+Zitb8BVTJwQxH0EbGpUdzgkTjnxhMFmxMEQP8WCI # hFRDDNdNuDgIs0Ldk6zWczBXJoKjRQ3Q6vVHgc2/JGAyWGBG8lhHhjKEHnRhZ5Ff # gVSxz5NMksHEpl3RYRNuKMYa+YaAu99h/EbBJx0kZxJyGiGKr0tkiVBisV39dx89 # 8Fd1rL2KQk1AUdEPnAY+Z3/1ZsADlkR+79BL/W7lmsqxqPJ6Kgox8NpOBpG2iAg1 # 6HgcsOmZzTznL0S6p/TcZL2kAcEgCZN4zfy8wMlEXV4WnAEFTyJNAgMBAAGjggHm # MIIB4jAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU1WM6XIoxkPNDe3xGG8Uz # aFqFbVUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8G # A1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQw # VgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9j # cmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUF # BwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br # aS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwgaAGA1UdIAEB/wSB # lTCBkjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIBFjFodHRwOi8vd3d3Lm1p # Y3Jvc29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQuaHRtMEAGCCsGAQUFBwIC # MDQeMiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8AUwB0AGEAdABlAG0AZQBu # AHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAH5ohRDeLG4Jg/gXEDPZ2joSFvs+um # zPUxvs8F4qn++ldtGTCzwsVmyWrf9efweL3HqJ4l4/m87WtUVwgrUYJEEvu5U4zM # 9GASinbMQEBBm9xcF/9c+V4XNZgkVkt070IQyK+/f8Z/8jd9Wj8c8pl5SpFSAK84 # Dxf1L3mBZdmptWvkx872ynoAb0swRCQiPM/tA6WWj1kpvLb9BOFwnzJKJ/1Vry/+ # tuWOM7tiX5rbV0Dp8c6ZZpCM/2pif93FSguRJuI57BlKcWOdeyFtw5yjojz6f32W # apB4pm3S4Zz5Hfw42JT0xqUKloakvZ4argRCg7i1gJsiOCC1JeVk7Pf0v35jWSUP # ei45V3aicaoGig+JFrphpxHLmtgOR5qAxdDNp9DvfYPw4TtxCd9ddJgiCGHasFAe # b73x4QDf5zEHpJM692VHeOj4qEir995yfmFrb3epgcunCaw5u+zGy9iCtHLNHfS4 # hQEegPsbiSpUObJb2sgNVZl6h3M7COaYLeqN4DMuEin1wC9UJyH3yKxO2ii4sanb # lrKnQqLJzxlBTeCG+SqaoxFmMNO7dDJL32N79ZmKLxvHIa9Zta7cRDyXUHHXodLF # VeNp3lfB0d4wwP3M5k37Db9dT+mdHhk4L7zPWAUu7w2gUDXa7wknHNWzfjUeCLra # NtvTX4/edIhJEqGCAtIwggI7AgEBMIH8oYHUpIHRMIHOMQswCQYDVQQGEwJVUzET # MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQLEyBNaWNyb3NvZnQgT3BlcmF0 # aW9ucyBQdWVydG8gUmljbzEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046RDlERS1F # MzlBLTQzRkUxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wi # IwoBATAHBgUrDgMCGgMVAJ/OX8d+h3uxdL4JslJc9sPNpdCxoIGDMIGApH4wfDEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWlj # cm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDjqlat # MCIYDzIwMjEwMTE0MDkzNTQxWhgPMjAyMTAxMTUwOTM1NDFaMHcwPQYKKwYBBAGE # WQoEATEvMC0wCgIFAOOqVq0CAQAwCgIBAAICHhwCAf8wBwIBAAICERwwCgIFAOOr # qC0CAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAweh # IKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQC4HuVGL7tyUUNjto21JsPh # pzYQR70/LP+Omawmi17OWar1XwBeniQTKKP6b31S6oMG+ZBsjiwvv1Will2k+eIu # tVCO1/SBaLhBOi1jCSEpt2BE9ob/LdH/ZpM1TpLYDAYSTEoD22G8jo85mnaKpAPr # /KYHJxP61wElczmUt6AwajGCAw0wggMJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0 # YW1wIFBDQSAyMDEwAhMzAAABLS5NQcpjZTOgAAAAAAEtMA0GCWCGSAFlAwQCAQUA # oIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIE # ICZzD/Jm0UkvxxWTy7tq5gijna+swNGvxpu8Mz++N97QMIH6BgsqhkiG9w0BCRAC # LzGB6jCB5zCB5DCBvQQgjvFacnJ9IAeP4pUQDzw20vgpm6o/7gtNdZqHOaHhg8Ew # gZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G # A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYw # JAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAS0uTUHK # Y2UzoAAAAAABLTAiBCA4hqNcH+sGx4tMcfX2jFhRGQR71oufu1BEpWcJw3qYZjAN # BgkqhkiG9w0BAQsFAASCAQBQzs5ew7aHZb0WdUhenJP6F2vKI42kKS2vE2leV7LT # sar6uY2vXsSGurLmBnM1ArTS2OHUZFy6+gS1aSYCLVjqGtel2/JxFIs0R05RGUgX # ii1M1a4N5Iw0nIhPc8GZYKk3CD6/op+470r6jotm3uY0Pn5pT5ayE5MxyKGz5JFQ # ngtT1iO1VnmCYO5GDwbcDAiLlio8AkE9TcMGWcZ2d6Lex2D2DXHsO986Fy/MSTqE # CfN2EWSVDpWNDwNt4bvTsMfuzqB9l2WvpExqIai3PBJAITyWtF3pkiucMph70vTE # Be61mO2DQtZl94x3WsFbqtXFkWa5cln2iDPXNWb5JYmU # SIG # End signature block |